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