AvoidDuplicateLiteralsRule.java
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"12041.0f);
031 
032     public static final IntegerProperty MINIMUM_LENGTH_DESCRIPTOR = new IntegerProperty("minimumLength",
033             "Minimum string length to check"1, Integer.MAX_VALUE, 31.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 = + 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 }