/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.design; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.java.rule.regex.RegexHelper; import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty; import net.sourceforge.pmd.lang.rule.properties.StringProperty; /** * <p> * A generic rule that can be configured to "count" classes of certain type * based on either their name (full name, prefix, suffixes anything can be * matched with a regex), and/or their type. * </p> * * <p>Example of configurations:</p> * * <pre> * <!-- Property order is MANDATORY !!! --> * <!-- Several regexes may be provided to ensure a match... --> * <property name="nameMatch" description="a regex on which to match" * value="^Abstract.*Bean*$,^*EJB*$"/> * <!-- An operand to refine match strategy TODO: Not implemented yet !!! --> * <property name"operand" description="" * value="and"/> <!-- possible values are and/or --> * <!-- Must be a full name to ensure type control !!! --> * <property name="typeMatch" description="a regex to match on implements/extends classname" * value="javax.servlet.Filter"/> * <!-- Define after how many occurences one should log a violation --> * <property name="threshold" description="Defines how many occurences are legal" * value="2"/> * <!-- TODO: Add a parameter to allow "ignore" pattern based on name --> * </pre> * * @author Ryan Gutafson, rgustav@users.sourceforge.net * @author Romain PELISSE, belaran@gmail.com * */ public class GenericClassCounterRule extends AbstractJavaRule { private static final StringMultiProperty NAME_MATCH_DESCRIPTOR = new StringMultiProperty("nameMatch", "A series of regex, separated by ',' to match on the classname", new String[] { "" }, 1.0f, ','); private static final StringProperty OPERAND_DESCRIPTOR = new StringProperty("operand", "or/and value to refined match criteria", new String(), 2.0f); private static final StringMultiProperty TYPE_MATCH_DESCRIPTOR = new StringMultiProperty("typeMatch", "A series of regex, separated by ',' to match on implements/extends classname", new String[] { "" }, 3.0f, ','); // TODO - this should be an IntegerProperty instead? private static final StringProperty THRESHOLD_DESCRIPTOR = new StringProperty("threshold", "Defines how many occurences are legal", new String(), 4.0f); private List<Pattern> namesMatch = new ArrayList<>(0); private List<Pattern> typesMatch = new ArrayList<>(0); private List<Node> matches = new ArrayList<>(0); private List<String> simpleClassname = new ArrayList<>(0); // When the rule is finished, this field will be used. @SuppressWarnings("PMD") private String operand; private int threshold; private static String counterLabel; public GenericClassCounterRule() { definePropertyDescriptor(NAME_MATCH_DESCRIPTOR); definePropertyDescriptor(OPERAND_DESCRIPTOR); definePropertyDescriptor(TYPE_MATCH_DESCRIPTOR); definePropertyDescriptor(THRESHOLD_DESCRIPTOR); } private List<String> arrayAsList(String[] array) { List<String> list = new ArrayList<>(array.length); int nbItem = 0; while (nbItem < array.length) { list.add(array[nbItem++]); } return list; } protected void init() { // Creating the attribute name for the rule context counterLabel = this.getClass().getSimpleName() + ".number of match"; // Constructing the request from the input parameters this.namesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(NAME_MATCH_DESCRIPTOR))); this.operand = getProperty(OPERAND_DESCRIPTOR); this.typesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(TYPE_MATCH_DESCRIPTOR))); String thresholdAsString = getProperty(THRESHOLD_DESCRIPTOR); this.threshold = Integer.valueOf(thresholdAsString); // Initializing list of match this.matches = new ArrayList<>(); } @Override public void start(RuleContext ctx) { // Adding the proper attribute to the context ctx.setAttribute(counterLabel, new AtomicLong()); super.start(ctx); } @Override public Object visit(ASTCompilationUnit node, Object data) { init(); return super.visit(node, data); } @Override public Object visit(ASTImportDeclaration node, Object data) { // Is there any imported types that match ? for (Pattern pattern : this.typesMatch) { if (RegexHelper.isMatch(pattern, node.getImportedName())) { if (simpleClassname == null) { simpleClassname = new ArrayList<>(1); } simpleClassname.add(node.getImportedName()); } // FIXME: use type resolution framework to deal with star import ? } return super.visit(node, data); } @Override public Object visit(ASTClassOrInterfaceType classType, Object data) { // Is extends/implements list using one of the previous match on import ? // FIXME: use type resolution framework to deal with star import ? for (String matchType : simpleClassname) { if (searchForAMatch(matchType, classType)) { addAMatch(classType, data); } } // TODO: implements the "operand" functionnality // Is there any names that actually match ? for (Pattern pattern : this.namesMatch) { if (RegexHelper.isMatch(pattern, classType.getImage())) { addAMatch(classType, data); } } return super.visit(classType, data); } private void addAMatch(Node node, Object data) { // We have a match, we increment RuleContext ctx = (RuleContext) data; AtomicLong total = (AtomicLong) ctx.getAttribute(counterLabel); total.incrementAndGet(); // And we keep a ref on the node for the report generation this.matches.add(node); } private boolean searchForAMatch(String matchType, Node node) { String xpathQuery = "//ClassOrInterfaceDeclaration[(./ExtendsList/ClassOrInterfaceType[@Image = '" + matchType + "']) or (./ImplementsList/ClassOrInterfaceType[@Image = '" + matchType + "'])]"; return node.hasDescendantMatchingXPath(xpathQuery); } @Override public void end(RuleContext ctx) { AtomicLong total = (AtomicLong) ctx.getAttribute(counterLabel); // Do we have a violation ? if (total.get() > this.threshold) { for (Node node : this.matches) { addViolation(ctx, node, new Object[] { total }); } // Cleaning the context for the others rules ctx.removeAttribute(counterLabel); super.end(ctx); } } }