/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.strings; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.IOUtils; import net.sourceforge.pmd.PropertySource; import net.sourceforge.pmd.lang.java.ast.ASTAnnotation; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.rule.properties.BooleanProperty; import net.sourceforge.pmd.lang.rule.properties.CharacterProperty; import net.sourceforge.pmd.lang.rule.properties.FileProperty; import net.sourceforge.pmd.lang.rule.properties.IntegerProperty; import net.sourceforge.pmd.lang.rule.properties.StringProperty; import net.sourceforge.pmd.util.StringUtil; public class AvoidDuplicateLiteralsRule extends AbstractJavaRule { public static final IntegerProperty THRESHOLD_DESCRIPTOR = new IntegerProperty("maxDuplicateLiterals", "Max duplicate literals", 1, 20, 4, 1.0f); public static final IntegerProperty MINIMUM_LENGTH_DESCRIPTOR = new IntegerProperty("minimumLength", "Minimum string length to check", 1, Integer.MAX_VALUE, 3, 1.5f); public static final BooleanProperty SKIP_ANNOTATIONS_DESCRIPTOR = new BooleanProperty("skipAnnotations", "Skip literals within annotations", false, 2.0f); public static final StringProperty EXCEPTION_LIST_DESCRIPTOR = new StringProperty("exceptionList", "Strings to ignore", null, 3.0f); public static final CharacterProperty SEPARATOR_DESCRIPTOR = new CharacterProperty("separator", "Ignore list separator", ',', 4.0f); public static final FileProperty EXCEPTION_FILE_DESCRIPTOR = new FileProperty("exceptionfile", "File containing strings to skip (one string per line), only used if ignore list is not set", null, 5.0f); public static class ExceptionParser { private static final char ESCAPE_CHAR = '\\'; private char delimiter; public ExceptionParser(char delimiter) { this.delimiter = delimiter; } public Set<String> parse(String s) { Set<String> result = new HashSet<>(); StringBuilder currentToken = new StringBuilder(); boolean inEscapeMode = false; for (int i = 0; i < s.length(); i++) { if (inEscapeMode) { inEscapeMode = false; currentToken.append(s.charAt(i)); continue; } if (s.charAt(i) == ESCAPE_CHAR) { inEscapeMode = true; continue; } if (s.charAt(i) == delimiter) { result.add(currentToken.toString()); currentToken = new StringBuilder(); } else { currentToken.append(s.charAt(i)); } } if (currentToken.length() > 0) { result.add(currentToken.toString()); } return result; } } private Map<String, List<ASTLiteral>> literals = new HashMap<>(); private Set<String> exceptions = new HashSet<>(); private int minLength; public AvoidDuplicateLiteralsRule() { definePropertyDescriptor(THRESHOLD_DESCRIPTOR); definePropertyDescriptor(MINIMUM_LENGTH_DESCRIPTOR); definePropertyDescriptor(SKIP_ANNOTATIONS_DESCRIPTOR); definePropertyDescriptor(EXCEPTION_LIST_DESCRIPTOR); definePropertyDescriptor(SEPARATOR_DESCRIPTOR); definePropertyDescriptor(EXCEPTION_FILE_DESCRIPTOR); } private LineNumberReader getLineReader() throws FileNotFoundException { return new LineNumberReader(new BufferedReader(new FileReader(getProperty(EXCEPTION_FILE_DESCRIPTOR)))); } @Override public Object visit(ASTCompilationUnit node, Object data) { literals.clear(); if (getProperty(EXCEPTION_LIST_DESCRIPTOR) != null) { ExceptionParser p = new ExceptionParser(getProperty(SEPARATOR_DESCRIPTOR)); exceptions = p.parse(getProperty(EXCEPTION_LIST_DESCRIPTOR)); } else if (getProperty(EXCEPTION_FILE_DESCRIPTOR) != null) { exceptions = new HashSet<>(); LineNumberReader reader = null; try { reader = getLineReader(); String line; while ((line = reader.readLine()) != null) { exceptions.add(line); } } catch (IOException ioe) { ioe.printStackTrace(); } finally { IOUtils.closeQuietly(reader); } } minLength = 2 + getProperty(MINIMUM_LENGTH_DESCRIPTOR); super.visit(node, data); processResults(data); return data; } private void processResults(Object data) { int threshold = getProperty(THRESHOLD_DESCRIPTOR); for (Map.Entry<String, List<ASTLiteral>> entry : literals.entrySet()) { List<ASTLiteral> occurrences = entry.getValue(); if (occurrences.size() >= threshold) { ASTLiteral first = occurrences.get(0); String rawImage = first.getEscapedStringLiteral(); Object[] args = new Object[] { rawImage, Integer.valueOf(occurrences.size()), Integer.valueOf(first.getBeginLine()), }; addViolation(data, first, args); } } } @Override public Object visit(ASTLiteral node, Object data) { if (!node.isStringLiteral()) { return data; } String image = node.getImage(); // just catching strings of 'minLength' chars or more (including the // enclosing quotes) if (image.length() < minLength) { return data; } // skip any exceptions if (exceptions.contains(image.substring(1, image.length() - 1))) { return data; } // Skip literals in annotations if (getProperty(SKIP_ANNOTATIONS_DESCRIPTOR) && node.getFirstParentOfType(ASTAnnotation.class) != null) { return data; } if (literals.containsKey(image)) { List<ASTLiteral> occurrences = literals.get(image); occurrences.add(node); } else { List<ASTLiteral> occurrences = new ArrayList<>(); occurrences.add(node); literals.put(image, occurrences); } return data; } private static String checkFile(File file) { if (!file.exists()) { return "File '" + file.getName() + "' does not exist"; } if (!file.canRead()) { return "File '" + file.getName() + "' cannot be read"; } if (file.length() == 0) { return "File '" + file.getName() + "' is empty"; } return null; } /** * @see PropertySource#dysfunctionReason() */ @Override public String dysfunctionReason() { File file = getProperty(EXCEPTION_FILE_DESCRIPTOR); if (file != null) { String issue = checkFile(file); if (issue != null) { return issue; } String ignores = getProperty(EXCEPTION_LIST_DESCRIPTOR); if (StringUtil.isNotEmpty(ignores)) { return "Cannot reference external file AND local values"; } } return null; } }