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.design;
5   
6   import java.util.ArrayList;
7   import java.util.List;
8   import java.util.concurrent.atomic.AtomicLong;
9   import java.util.regex.Pattern;
10  
11  import net.sourceforge.pmd.RuleContext;
12  import net.sourceforge.pmd.lang.ast.Node;
13  import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
14  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
15  import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
16  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
17  import net.sourceforge.pmd.lang.java.rule.regex.RegexHelper;
18  import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
19  import net.sourceforge.pmd.lang.rule.properties.StringProperty;
20  
21  /**
22   * <p>A generic rule that can be configured to "count" classes of certain
23   * type based on either their name (full name, prefix, suffixes anything can
24   * be matched with a regex), and/or
25   * their type.</p>
26   *
27   * <p>Example of configurations:
28   * 		<!-- Property order is MANDATORY !!! -->
29   * 		<!-- Several regexes may be provided to ensure a match... -->
30   * 		<property 	name="nameMatch" description="a regex on which to match"
31   * 					value="^Abstract.*Bean*$,^*EJB*$"/>
32   * 		<!-- An operand to refine match strategy TODO: Not implemented yet !!! -->
33   * 		<property 	name"operand"	description=""
34   * 					value="and"/> <!-- possible values are and/or -->
35   * 		<!-- Must be a full name to ensure type control !!! -->
36   * 		<property 	name="typeMatch" description="a regex to match on implements/extends classname"
37   * 					value="javax.servlet.Filter"/>
38   * 		<!-- Define after how many occurences one should log a violation -->
39   * 		<property 	name="threshold"	description="Defines how many occurences are legal"
40   * 					value="2"/>
41   * 		<!-- TODO: Add a parameter to allow "ignore" pattern based on name -->
42   * </p>
43   *
44   * @author Ryan Gutafson, rgustav@users.sourceforge.net
45   * @author Romain PELISSE, belaran@gmail.com
46   *
47   */
48  public class GenericClassCounterRule extends AbstractJavaRule {
49  
50  
51  	private static final StringMultiProperty NAME_MATCH_DESCRIPTOR = new StringMultiProperty("nameMatch",
52  			"A series of regex, separated by ',' to match on the classname", new String[] {""},1.0f,',');
53  
54  	private static final StringProperty OPERAND_DESCRIPTOR = new StringProperty("operand",
55  			"or/and value to refined match criteria",new String(),2.0f);
56  
57  	private static final StringMultiProperty TYPE_MATCH_DESCRIPTOR = new StringMultiProperty("typeMatch",
58  			"A series of regex, separated by ',' to match on implements/extends classname",new String[]{""},3.0f,',');
59  
60  	// TODO - this should be an IntegerProperty instead?
61  	private static final StringProperty THRESHOLD_DESCRIPTOR = new StringProperty("threshold",
62  			"Defines how many occurences are legal",new String(),4.0f);
63  
64  
65  	private List<Pattern> namesMatch = new ArrayList<>(0);
66  	private List<Pattern> typesMatch = new ArrayList<>(0);
67  	private List<Node> matches = new ArrayList<>(0);
68  	private List<String> simpleClassname = new ArrayList<>(0);
69  
70  
71  	@SuppressWarnings("PMD") // When the rule is finished, this field will be used.
72  	private String operand;
73  	private int threshold;
74  
75  	private static String counterLabel;
76  
77  	public GenericClassCounterRule() {
78  	    definePropertyDescriptor(NAME_MATCH_DESCRIPTOR);
79  	    definePropertyDescriptor(OPERAND_DESCRIPTOR);
80  	    definePropertyDescriptor(TYPE_MATCH_DESCRIPTOR);
81  	    definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
82  	}
83  
84  
85  	private List<String> arrayAsList(String[] array) {
86  		List<String> list = new ArrayList<>(array.length);
87  		int nbItem = 0;
88  		while (nbItem < array.length ) {
89  			list.add(array[nbItem++]);
90  		}
91  		return list;
92  	}
93  
94  	protected void init(){
95  		// Creating the attribute name for the rule context
96  		counterLabel = this.getClass().getSimpleName() + ".number of match";
97  		// Constructing the request from the input parameters
98  		this.namesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(NAME_MATCH_DESCRIPTOR)));
99  		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<>();
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<>(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.end(ctx);
182         }
183      }
184 }