1
2
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
162 if (image.length() < minLength) {
163 return data;
164 }
165
166
167 if (exceptions.contains(image.substring(1, image.length() - 1))) {
168 return data;
169 }
170
171
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
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 }