001 /**
002 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003 */
004 package net.sourceforge.pmd.lang.java.rule.strings;
005
006 import java.io.BufferedReader;
007 import java.io.File;
008 import java.io.FileReader;
009 import java.io.IOException;
010 import java.io.LineNumberReader;
011 import java.util.ArrayList;
012 import java.util.HashMap;
013 import java.util.HashSet;
014 import java.util.List;
015 import java.util.Map;
016 import java.util.Set;
017
018 import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
019 import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
020 import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
021 import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
022 import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
023 import net.sourceforge.pmd.lang.rule.properties.CharacterProperty;
024 import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
025 import net.sourceforge.pmd.lang.rule.properties.StringProperty;
026
027 public class AvoidDuplicateLiteralsRule extends AbstractJavaRule {
028
029 public static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("maxDuplicateLiterals",
030 "Max duplicate literals", 1, 20, 4, 1.0f);
031
032 public static final IntegerProperty MINIMUM_LENGTH_DESCRIPTOR = new IntegerProperty("minimumLength",
033 "Minimum string length to check", 1, Integer.MAX_VALUE, 3, 1.5f);
034
035 public static final BooleanProperty SKIP_ANNOTATIONS_DESCRIPTOR = new BooleanProperty("skipAnnotations",
036 "Skip literals within annotations", false, 2.0f);
037
038 public static final StringProperty EXCEPTION_LIST_DESCRIPTOR = new StringProperty("exceptionList",
039 "Strings in that list are skipped", null, 3.0f);
040
041 public static final CharacterProperty SEPARATOR_DESCRIPTOR = new CharacterProperty("separator",
042 "Exception list separator", ',', 4.0f);
043
044 public static final StringProperty EXCEPTION_FILE_DESCRIPTOR = new StringProperty("exceptionfile",
045 "File containing strings to skip (one string per line), only used if exceptionlist is not set", null, 5.0f);
046
047 public static class ExceptionParser {
048
049 private static final char ESCAPE_CHAR = '\\';
050 private char delimiter;
051
052 public ExceptionParser(char delimiter) {
053 this.delimiter = delimiter;
054 }
055
056 public Set<String> parse(String s) {
057 Set<String> result = new HashSet<String>();
058 StringBuffer currentToken = new StringBuffer();
059 boolean inEscapeMode = false;
060 for (int i = 0; i < s.length(); i++) {
061 if (inEscapeMode) {
062 inEscapeMode = false;
063 currentToken.append(s.charAt(i));
064 continue;
065 }
066 if (s.charAt(i) == ESCAPE_CHAR) {
067 inEscapeMode = true;
068 continue;
069 }
070 if (s.charAt(i) == delimiter) {
071 result.add(currentToken.toString());
072 currentToken = new StringBuffer();
073 } else {
074 currentToken.append(s.charAt(i));
075 }
076 }
077 if (currentToken.length() > 0) {
078 result.add(currentToken.toString());
079 }
080 return result;
081 }
082 }
083
084 private Map<String, List<ASTLiteral>> literals = new HashMap<String, List<ASTLiteral>>();
085 private Set<String> exceptions = new HashSet<String>();
086 private int minLength;
087
088 public AvoidDuplicateLiteralsRule() {
089 definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
090 definePropertyDescriptor(MINIMUM_LENGTH_DESCRIPTOR);
091 definePropertyDescriptor(SKIP_ANNOTATIONS_DESCRIPTOR);
092 definePropertyDescriptor(EXCEPTION_LIST_DESCRIPTOR);
093 definePropertyDescriptor(SEPARATOR_DESCRIPTOR);
094 definePropertyDescriptor(EXCEPTION_FILE_DESCRIPTOR);
095 }
096
097 @Override
098 public Object visit(ASTCompilationUnit node, Object data) {
099 literals.clear();
100
101 if (getProperty(EXCEPTION_LIST_DESCRIPTOR) != null) {
102 ExceptionParser p = new ExceptionParser(getProperty(SEPARATOR_DESCRIPTOR));
103 exceptions = p.parse(getProperty(EXCEPTION_LIST_DESCRIPTOR));
104 } else if (getProperty(EXCEPTION_FILE_DESCRIPTOR) != null) {
105 exceptions = new HashSet<String>();
106 LineNumberReader reader = null;
107 try {
108 reader = new LineNumberReader(new BufferedReader(new FileReader(new File(
109 getProperty(EXCEPTION_FILE_DESCRIPTOR)))));
110 String line;
111 while ((line = reader.readLine()) != null) {
112 exceptions.add(line);
113 }
114 } catch (IOException ioe) {
115 ioe.printStackTrace();
116 } finally {
117 try {
118 if (reader != null) {
119 reader.close();
120 }
121 } catch (IOException ioe) {
122 ioe.printStackTrace();
123 }
124 }
125 }
126
127 super.visit(node, data);
128
129 int threshold = getProperty(THRESHOLD_DESCRIPTOR);
130 for (String key : literals.keySet()) {
131 List<ASTLiteral> occurrences = literals.get(key);
132 if (occurrences.size() >= threshold) {
133 Object[] args = new Object[] { key, Integer.valueOf(occurrences.size()),
134 Integer.valueOf(occurrences.get(0).getBeginLine()) };
135 addViolation(data, occurrences.get(0), args);
136 }
137 }
138
139 minLength = 2 + getProperty(MINIMUM_LENGTH_DESCRIPTOR);
140
141 return data;
142 }
143
144 @Override
145 public Object visit(ASTLiteral node, Object data) {
146 if (!node.isStringLiteral()) {
147 return data;
148 }
149 String image = node.getImage();
150
151 // just catching strings of 'minLength' chars or more (including the enclosing quotes)
152 if (image.length() < minLength) {
153 return data;
154 }
155
156 // skip any exceptions
157 if (exceptions.contains(image.substring(1, image.length() - 1))) {
158 return data;
159 }
160
161 // Skip literals in annotations
162 if (getProperty(SKIP_ANNOTATIONS_DESCRIPTOR) && node.getFirstParentOfType(ASTAnnotation.class) != null) {
163 return data;
164 }
165
166 if (literals.containsKey(image)) {
167 List<ASTLiteral> occurrences = literals.get(image);
168 occurrences.add(node);
169 } else {
170 List<ASTLiteral> occurrences = new ArrayList<ASTLiteral>();
171 occurrences.add(node);
172 literals.put(image, occurrences);
173 }
174
175 return data;
176 }
177 }
|