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 }