View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.strings;
5   
6   import java.io.BufferedReader;
7   import java.io.File;
8   import java.io.FileNotFoundException;
9   import java.io.FileReader;
10  import java.io.IOException;
11  import java.io.LineNumberReader;
12  import java.util.ArrayList;
13  import java.util.HashMap;
14  import java.util.HashSet;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Set;
18  
19  import net.sourceforge.pmd.PropertySource;
20  import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
21  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
22  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
23  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
24  import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
25  import net.sourceforge.pmd.lang.rule.properties.CharacterProperty;
26  import net.sourceforge.pmd.lang.rule.properties.FileProperty;
27  import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
28  import net.sourceforge.pmd.lang.rule.properties.StringProperty;
29  import net.sourceforge.pmd.util.StringUtil;
30  
31  import org.apache.commons.io.IOUtils;
32  
33  public class AvoidDuplicateLiteralsRule extends AbstractJavaRule {
34  
35      public static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("maxDuplicateLiterals",
36              "Max duplicate literals", 1, 20, 4, 1.0f);
37  
38      public static final IntegerProperty MINIMUM_LENGTH_DESCRIPTOR = new IntegerProperty("minimumLength",
39              "Minimum string length to check", 1, Integer.MAX_VALUE, 3, 1.5f);
40  
41      public static final BooleanProperty SKIP_ANNOTATIONS_DESCRIPTOR = new BooleanProperty("skipAnnotations",
42              "Skip literals within annotations", false, 2.0f);
43  
44      public static final StringProperty EXCEPTION_LIST_DESCRIPTOR = new StringProperty("exceptionList",
45              "Strings to ignore", null, 3.0f);
46  
47      public static final CharacterProperty SEPARATOR_DESCRIPTOR = new CharacterProperty("separator",
48              "Ignore list separator", ',', 4.0f);
49  
50      public static final FileProperty EXCEPTION_FILE_DESCRIPTOR = new FileProperty("exceptionfile",
51              "File containing strings to skip (one string per line), only used if ignore list is not set", null, 5.0f);
52  
53      public static class ExceptionParser {
54  
55          private static final char ESCAPE_CHAR = '\\';
56          private char delimiter;
57  
58          public ExceptionParser(char delimiter) {
59              this.delimiter = delimiter;
60          }
61  
62          public Set<String> parse(String s) {
63              Set<String> result = new HashSet<>();
64              StringBuilder currentToken = new StringBuilder();
65              boolean inEscapeMode = false;
66              for (int i = 0; i < s.length(); i++) {
67                  if (inEscapeMode) {
68                      inEscapeMode = false;
69                      currentToken.append(s.charAt(i));
70                      continue;
71                  }
72                  if (s.charAt(i) == ESCAPE_CHAR) {
73                      inEscapeMode = true;
74                      continue;
75                  }
76                  if (s.charAt(i) == delimiter) {
77                      result.add(currentToken.toString());
78                      currentToken = new StringBuilder();
79                  } else {
80                      currentToken.append(s.charAt(i));
81                  }
82              }
83              if (currentToken.length() > 0) {
84                  result.add(currentToken.toString());
85              }
86              return result;
87          }
88      }
89  
90      private Map<String, List<ASTLiteral>> literals = new HashMap<>();
91      private Set<String> exceptions = new HashSet<>();
92      private int minLength;
93  
94      public AvoidDuplicateLiteralsRule() {
95          definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
96          definePropertyDescriptor(MINIMUM_LENGTH_DESCRIPTOR);
97          definePropertyDescriptor(SKIP_ANNOTATIONS_DESCRIPTOR);
98          definePropertyDescriptor(EXCEPTION_LIST_DESCRIPTOR);
99          definePropertyDescriptor(SEPARATOR_DESCRIPTOR);
100         definePropertyDescriptor(EXCEPTION_FILE_DESCRIPTOR);
101     }
102 
103     private LineNumberReader getLineReader() throws FileNotFoundException {
104         return new LineNumberReader(new BufferedReader(new FileReader(getProperty(EXCEPTION_FILE_DESCRIPTOR))));
105     }
106 
107     @Override
108     public Object visit(ASTCompilationUnit node, Object data) {
109         literals.clear();
110 
111         if (getProperty(EXCEPTION_LIST_DESCRIPTOR) != null) {
112             ExceptionParser p = new ExceptionParser(getProperty(SEPARATOR_DESCRIPTOR));
113             exceptions = p.parse(getProperty(EXCEPTION_LIST_DESCRIPTOR));
114         } else if (getProperty(EXCEPTION_FILE_DESCRIPTOR) != null) {
115             exceptions = new HashSet<>();
116             LineNumberReader reader = null;
117             try {
118                 reader = getLineReader();
119                 String line;
120                 while ((line = reader.readLine()) != null) {
121                     exceptions.add(line);
122                 }
123             } catch (IOException ioe) {
124                 ioe.printStackTrace();
125             } finally {
126                 IOUtils.closeQuietly(reader);
127             }
128         }
129 
130         minLength = 2 + getProperty(MINIMUM_LENGTH_DESCRIPTOR);
131 
132         super.visit(node, data);
133 
134         processResults(data);
135 
136         return data;
137     }
138 
139     private void processResults(Object data) {
140 
141         int threshold = getProperty(THRESHOLD_DESCRIPTOR);
142 
143         for (Map.Entry<String, List<ASTLiteral>> entry : literals.entrySet()) {
144             List<ASTLiteral> occurrences = entry.getValue();
145             if (occurrences.size() >= threshold) {
146                 ASTLiteral first = occurrences.get(0);
147                 String rawImage = first.getEscapedStringLiteral();
148                 Object[] args = new Object[] { rawImage, Integer.valueOf(occurrences.size()),
149                         Integer.valueOf(first.getBeginLine()) };
150                 addViolation(data, first, args);
151             }
152         }
153     }
154 
155     @Override
156     public Object visit(ASTLiteral node, Object data) {
157         if (!node.isStringLiteral()) {
158             return data;
159         }
160         String image = node.getImage();
161 
162         // just catching strings of 'minLength' chars or more (including the
163         // enclosing quotes)
164         if (image.length() < minLength) {
165             return data;
166         }
167 
168         // skip any exceptions
169         if (exceptions.contains(image.substring(1, image.length() - 1))) {
170             return data;
171         }
172 
173         // Skip literals in annotations
174         if (getProperty(SKIP_ANNOTATIONS_DESCRIPTOR) && node.getFirstParentOfType(ASTAnnotation.class) != null) {
175             return data;
176         }
177 
178         if (literals.containsKey(image)) {
179             List<ASTLiteral> occurrences = literals.get(image);
180             occurrences.add(node);
181         } else {
182             List<ASTLiteral> occurrences = new ArrayList<>();
183             occurrences.add(node);
184             literals.put(image, occurrences);
185         }
186 
187         return data;
188     }
189 
190     private static String checkFile(File file) {
191 
192         if (!file.exists()) {
193             return "File '" + file.getName() + "' does not exist";
194         }
195         if (!file.canRead()) {
196             return "File '" + file.getName() + "' cannot be read";
197         }
198         if (file.length() == 0) {
199             return "File '" + file.getName() + "' is empty";
200         }
201 
202         return null;
203     }
204 
205     /**
206      * @see PropertySource#dysfunctionReason()
207      */
208     @Override
209     public String dysfunctionReason() {
210 
211         File file = getProperty(EXCEPTION_FILE_DESCRIPTOR);
212         if (file != null) {
213             String issue = checkFile(file);
214             if (issue != null) {
215                 return issue;
216             }
217 
218             String ignores = getProperty(EXCEPTION_LIST_DESCRIPTOR);
219             if (StringUtil.isNotEmpty(ignores)) {
220                 return "Cannot reference external file AND local values";
221             }
222         }
223 
224         return null;
225     }
226 }