001 /**
002 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003 */
004 package net.sourceforge.pmd.lang.java.rule.design;
005
006 import java.util.ArrayList;
007 import java.util.List;
008 import java.util.concurrent.atomic.AtomicLong;
009 import java.util.regex.Pattern;
010
011 import net.sourceforge.pmd.RuleContext;
012 import net.sourceforge.pmd.lang.ast.Node;
013 import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
014 import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
015 import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
016 import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
017 import net.sourceforge.pmd.lang.java.rule.regex.RegexHelper;
018 import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
019 import net.sourceforge.pmd.lang.rule.properties.StringProperty;
020
021 /**
022 * <p>A generic rule that can be configured to "count" classes of certain
023 * type based on either their name (full name, prefix, suffixes anything can
024 * be matched with a regex), and/or
025 * their type.</p>
026 *
027 * <p>Example of configurations:
028 * <!-- Property order is MANDATORY !!! -->
029 * <!-- Several regexes may be provided to ensure a match... -->
030 * <property name="nameMatch" description="a regex on which to match"
031 * value="^Abstract.*Bean*$,^*EJB*$"/>
032 * <!-- An operand to refine match strategy TODO: Not implemented yet !!! -->
033 * <property name"operand" description=""
034 * value="and"/> <!-- possible values are and/or -->
035 * <!-- Must be a full name to ensure type control !!! -->
036 * <property name="typeMatch" description="a regex to match on implements/extends classname"
037 * value="javax.servlet.Filter"/>
038 * <!-- Define after how many occurences one should log a violation -->
039 * <property name="threshold" description="Defines how many occurences are legal"
040 * value="2"/>
041 * <!-- TODO: Add a parameter to allow "ignore" pattern based on name -->
042 * </p>
043 *
044 * @author Ryan Gutafson, rgustav@users.sourceforge.net
045 * @author Romain PELISSE, belaran@gmail.com
046 *
047 */
048 public class GenericClassCounterRule extends AbstractJavaRule {
049
050
051 private static final StringMultiProperty NAME_MATCH_DESCRIPTOR = new StringMultiProperty("nameMatch",
052 "A series of regex, separated by ',' to match on the classname", new String[] {""},1.0f,',');
053
054 private static final StringProperty OPERAND_DESCRIPTOR = new StringProperty("operand",
055 "or/and value to refined match criteria",new String(),2.0f);
056
057 private static final StringMultiProperty TYPE_MATCH_DESCRIPTOR = new StringMultiProperty("typeMatch",
058 "A series of regex, separated by ',' to match on implements/extends classname",new String[]{""},3.0f,',');
059
060 // TODO - this should be an IntegerProperty instead?
061 private static final StringProperty THRESHOLD_DESCRIPTOR = new StringProperty("threshold",
062 "Defines how many occurences are legal",new String(),4.0f);
063
064
065 private List<Pattern> namesMatch = new ArrayList<Pattern>(0);
066 private List<Pattern> typesMatch = new ArrayList<Pattern>(0);
067 private List<Node> matches = new ArrayList<Node>(0);
068 private List<String> simpleClassname = new ArrayList<String>(0);
069
070
071 @SuppressWarnings("PMD") // When the rule is finished, this field will be used.
072 private String operand;
073 private int threshold;
074
075 private static String counterLabel;
076
077 public GenericClassCounterRule() {
078 definePropertyDescriptor(NAME_MATCH_DESCRIPTOR);
079 definePropertyDescriptor(OPERAND_DESCRIPTOR);
080 definePropertyDescriptor(TYPE_MATCH_DESCRIPTOR);
081 definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
082 }
083
084
085 private List<String> arrayAsList(String[] array) {
086 List<String> list = new ArrayList<String>(array.length);
087 int nbItem = 0;
088 while (nbItem < array.length ) {
089 list.add(array[nbItem++]);
090 }
091 return list;
092 }
093
094 protected void init(){
095 // Creating the attribute name for the rule context
096 counterLabel = this.getClass().getSimpleName() + ".number of match";
097 // Constructing the request from the input parameters
098 this.namesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(NAME_MATCH_DESCRIPTOR)));
099 this.operand = getProperty(OPERAND_DESCRIPTOR);
100 this.typesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(TYPE_MATCH_DESCRIPTOR)));
101 String thresholdAsString = getProperty(THRESHOLD_DESCRIPTOR);
102 this.threshold = Integer.valueOf(thresholdAsString);
103 // Initializing list of match
104 this.matches = new ArrayList<Node>();
105
106 }
107
108 @Override
109 public void start(RuleContext ctx) {
110 // Adding the proper attribute to the context
111 ctx.setAttribute(counterLabel, new AtomicLong());
112 super.start(ctx);
113 }
114
115 @Override
116 public Object visit(ASTCompilationUnit node, Object data) {
117 init();
118 return super.visit(node,data);
119 }
120
121 @Override
122 public Object visit(ASTImportDeclaration node, Object data) {
123 // Is there any imported types that match ?
124 for (Pattern pattern : this.typesMatch) {
125 if ( RegexHelper.isMatch(pattern,node.getImportedName())) {
126 if ( simpleClassname == null ) {
127 simpleClassname = new ArrayList<String>(1);
128 }
129 simpleClassname.add(node.getImportedName());
130 }
131 // FIXME: use type resolution framework to deal with star import ?
132 }
133 return super.visit(node, data);
134 }
135
136 @Override
137 public Object visit(ASTClassOrInterfaceType classType,Object data) {
138 // Is extends/implements list using one of the previous match on import ?
139 // FIXME: use type resolution framework to deal with star import ?
140 for (String matchType : simpleClassname) {
141 if ( searchForAMatch(matchType,classType)) {
142 addAMatch(classType, data);
143 }
144 }
145 // TODO: implements the "operand" functionnality
146 // Is there any names that actually match ?
147 for (Pattern pattern : this.namesMatch) {
148 if ( RegexHelper.isMatch(pattern, classType.getImage())) {
149 addAMatch(classType, data);
150 }
151 }
152 return super.visit(classType, data);
153 }
154
155 private void addAMatch(Node node,Object data) {
156 // We have a match, we increment
157 RuleContext ctx = (RuleContext)data;
158 AtomicLong total = (AtomicLong)ctx.getAttribute(counterLabel);
159 total.incrementAndGet();
160 // And we keep a ref on the node for the report generation
161 this.matches.add(node);
162 }
163
164 private boolean searchForAMatch(String matchType, Node node) {
165 String xpathQuery = "//ClassOrInterfaceDeclaration[(./ExtendsList/ClassOrInterfaceType[@Image = '" + matchType
166 + "']) or (./ImplementsList/ClassOrInterfaceType[@Image = '" + matchType + "'])]";
167
168 return node.hasDescendantMatchingXPath(xpathQuery);
169 }
170
171 @Override
172 public void end(RuleContext ctx) {
173 AtomicLong total = (AtomicLong)ctx.getAttribute(counterLabel);
174 // Do we have a violation ?
175 if ( total.get() > this.threshold ) {
176 for (Node node : this.matches) {
177 addViolation(ctx,node , new Object[] { total });
178 }
179 // Cleaning the context for the others rules
180 ctx.removeAttribute(counterLabel);
181 super.start(ctx);
182 }
183 }
184 }
|