View Javadoc

1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.rules.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.AbstractJavaRule;
12  import net.sourceforge.pmd.PropertyDescriptor;
13  import net.sourceforge.pmd.RuleContext;
14  import net.sourceforge.pmd.ast.ASTClassOrInterfaceType;
15  import net.sourceforge.pmd.ast.ASTCompilationUnit;
16  import net.sourceforge.pmd.ast.ASTImportDeclaration;
17  import net.sourceforge.pmd.ast.SimpleNode;
18  import net.sourceforge.pmd.properties.StringProperty;
19  import net.sourceforge.pmd.rules.regex.RegexHelper;
20  
21  import org.jaxen.JaxenException;
22  
23  /**
24   * <p>A generic rule that can be configured to "count" classes of certain
25   * type based on either their name (full name, prefix, suffixes anything can
26   * be matched with a regex), and/or
27   * their type.</p>
28   *
29   * <p>Example of configurations:
30   * 		<!-- Property order is MANDATORY !!! -->
31   * 		<!-- Several regexes may be provided to ensure a match... -->
32   * 		<property 	name="nameMatch" description="a regex on which to match"
33   * 					value="^Abstract.*Bean*$,^*EJB*$"/>
34   * 		<!-- An operand to refine match strategy TODO: Not implemented yet !!! -->
35   * 		<property 	name"operand"	description=""
36   * 					value="and"/> <!-- possible values are and/or -->
37   * 		<!-- Must be a full name to ensure type control !!! -->
38   * 		<property 	name="typeMatch" description="a regex to match on implements/extends classname"
39   * 					value="javax.servlet.Filter"/>
40   * 		<!-- Define after how many occurences one should log a violation -->
41   * 		<property 	name="threshold"	description="Defines how many occurences are legal"
42   * 					value="2"/>
43   * 		<!-- TODO: Add a parameter to allow "ignore" pattern based on name -->
44   * </p>
45   *
46   * @author Ryan Gutafson, rgustav@users.sourceforge.net
47   * @author Romain PELISSE, belaran@gmail.com
48   *
49   */
50  public class GenericClassCounterRule extends AbstractJavaRule {
51  
52  
53  	private static final PropertyDescriptor nameMatchDescriptor = new StringProperty("nameMatch",
54  			"A series of regex, separeted by ',' to match on the classname", new String[] {""},1.0f,',');
55  
56  	private static final PropertyDescriptor operandDescriptor = new StringProperty("operand",
57  			"or/and value to refined match criteria",new String(),2.0f);
58  
59  	private static final PropertyDescriptor typeMatchDescriptor = new StringProperty("typeMatch",
60  			"A series of regex, separeted by ',' to match on implements/extends classname",new String[]{""},3.0f,',');
61  
62  	private static final PropertyDescriptor thresholdDescriptor = new StringProperty("threshold",
63  			"Defines how many occurences are legal",new String(),4.0f);
64  
65  
66  	private List<Pattern> namesMatch = new ArrayList<Pattern>(0);
67  	private List<Pattern> typesMatch = new ArrayList<Pattern>(0);
68  	private List<SimpleNode> matches = new ArrayList<SimpleNode>(0);
69  	private List<String> simpleClassname = new ArrayList<String>(0);
70  
71  
72  	@SuppressWarnings("PMD") // When the rule is finished, this field will be used.
73  	private String operand;
74  	private int threshold;
75  
76  	private static String COUNTER_LABEL;
77  
78  	/**
79  	 *	Default empty constructor
80  	 */
81  	public GenericClassCounterRule() {
82  		super();
83  	}
84  
85  	private List<String> arrayAsList(String[] array) {
86  		List<String> list = new ArrayList<String>(array.length);
87  		int nbItem = 0;
88  		while (nbItem < array.length )
89  			list.add(array[nbItem++]);
90  		return list;
91  	}
92  
93  	protected void init(){
94  		// Creating the attribute name for the rule context
95  		COUNTER_LABEL = this.getClass().getSimpleName() + ".number of match";
96  		// Constructing the request from the input parameters
97  		this.namesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getStringProperties(nameMatchDescriptor)));
98  		this.operand = getStringProperty(operandDescriptor);
99  		this.typesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getStringProperties(typeMatchDescriptor)));
100 		String thresholdAsString = getStringProperty(thresholdDescriptor);
101 		this.threshold = Integer.valueOf(thresholdAsString);
102 		// Initializing list of match
103 		this.matches = new ArrayList<SimpleNode>();
104 
105 	}
106 
107 	 @Override
108      public void start(RuleContext ctx) {
109 		 // Adding the proper attribute to the context
110          ctx.setAttribute(COUNTER_LABEL, new AtomicLong());
111          super.start(ctx);
112      }
113 
114      @Override
115      public Object visit(ASTCompilationUnit node, Object data) {
116     	 init();
117     	 return super.visit(node,data);
118      }
119 
120      @Override
121      public Object visit(ASTImportDeclaration node, Object data) {
122     	 // Is there any imported types that match ?
123     	 for (Pattern pattern : this.typesMatch) {
124     		 if ( RegexHelper.isMatch(pattern,node.getImportedName())) {
125     			 if ( simpleClassname == null )
126     				 simpleClassname = new ArrayList<String>(1);
127     			 simpleClassname.add(node.getImportedName());
128     		 }
129     		 // FIXME: use type resolution framework to deal with star import ?
130     	 }
131          return super.visit(node, data);
132      }
133 
134 	@Override
135 	public Object visit(ASTClassOrInterfaceType classType,Object data) {
136 		// Is extends/implements list using one of the previous match on import ?
137 		// FIXME: use type resolution framework to deal with star import ?
138 		for (String matchType : simpleClassname) {
139 			if ( searchForAMatch(matchType,classType)) {
140 				addAMatch(classType, data);
141 			}
142 		}
143 		// TODO: implements the "operand" functionnality
144 		// Is there any names that actually match ?
145 		for (Pattern pattern : this.namesMatch)
146 			if ( RegexHelper.isMatch(pattern, classType.getImage()))
147 				addAMatch(classType, data);
148 		return super.visit(classType, data);
149 	}
150 
151 	private void addAMatch(SimpleNode node,Object data) {
152 		// We have a match, we increment
153 		RuleContext ctx = (RuleContext)data;
154 		AtomicLong total = (AtomicLong)ctx.getAttribute(COUNTER_LABEL);
155 		total.incrementAndGet();
156 		// And we keep a ref on the node for the report generation
157 		this.matches.add(node);
158 	}
159 
160 	@SuppressWarnings("unchecked")
161     private boolean searchForAMatch(String matchType,SimpleNode node) {
162 		boolean status = false;
163     	 String xpathQuery = "//ClassOrInterfaceDeclaration[" +
164 							"(./ExtendsList/ClassOrInterfaceType[@Image = '" + matchType + "'])" +
165 							"or" +
166 							"(./ImplementsList/ClassOrInterfaceType[@Image = '" + matchType + "'])" +
167 							"]";
168 		try
169 		{
170 			List list = node.findChildNodesWithXPath(xpathQuery);
171 			if ( list != null && list.size() > 0 ) {
172 				// We got a match !
173 				status = true;
174 			}
175 		}
176 		catch (JaxenException e) {
177 			// Most likely, a should never happen exception...
178 			e.printStackTrace();
179 		}
180 		return status;
181 	}
182 
183 	@Override
184     public void end(RuleContext ctx) {
185 		AtomicLong total = (AtomicLong)ctx.getAttribute(COUNTER_LABEL);
186         // Do we have a violation ?
187         if ( total.get() > this.threshold )
188         	for (SimpleNode node : this.matches)
189         		addViolation(ctx,node , new Object[] { total });
190 		// Cleaning the context for the others rules
191 		ctx.removeAttribute(COUNTER_LABEL);
192 		super.start(ctx);
193      }
194 }