Skip to content

Commit 97705f5

Browse files
committed
Issue #79: Processing UT changes to get possible property values
1 parent f92362c commit 97705f5

File tree

9 files changed

+644
-1
lines changed

9 files changed

+644
-1
lines changed

config/import-control.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
<allow pkg="org.xml"/>
3535
</subpackage>
3636

37+
<subpackage name="customcheck">
38+
<allow pkg="com.puppycrawl.tools.checkstyle"/>
39+
</subpackage>
40+
3741
<subpackage name="data">
3842
<!-- it is only allowed until https://github.com/checkstyle/checkstyle/issues/3492 -->
3943
<allow pkg="org.immutables.gson"/>

config/pmd.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
<!-- we use class comments as source for xdoc files, so content is big and that is by design -->
3535
<exclude name="CommentSize"/>
3636
</rule>
37-
<rule ref="rulesets/java/comments.xml/CommentRequired" />
37+
<rule ref="rulesets/java/comments.xml/CommentRequired">
38+
<properties>
39+
<property name="violationSuppressXPath" value="//Annotation/MarkerAnnotation//Name[@Image='Override']"/>
40+
</properties>
41+
</rule>
3842
<rule ref="rulesets/java/comments.xml/CommentSize">
3943
<properties>
4044
<!-- we use class comments as source for xdoc files, so content is big and that is by design -->

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@
393393
<branchRate>0</branchRate>
394394
<lineRate>0</lineRate>
395395
</regex>
396+
<regex>
397+
<pattern>com.github.checkstyle.regression.customcheck.UnitTestProcessorCheck</pattern>
398+
<branchRate>61</branchRate>
399+
<lineRate>79</lineRate>
400+
</regex>
396401
<regex>
397402
<pattern>com.github.checkstyle.regression.report.ReportGenerator</pattern>
398403
<branchRate>0</branchRate>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle: Checks Java source code for adherence to a set of rules.
3+
// Copyright (C) 2001-2017 the original author or authors.
4+
//
5+
// This library is free software; you can redistribute it and/or
6+
// modify it under the terms of the GNU Lesser General Public
7+
// License as published by the Free Software Foundation; either
8+
// version 2.1 of the License, or (at your option) any later version.
9+
//
10+
// This library is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
// Lesser General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU Lesser General Public
16+
// License along with this library; if not, write to the Free Software
17+
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package com.github.checkstyle.regression.customcheck;
21+
22+
import java.io.File;
23+
import java.util.Collections;
24+
import java.util.List;
25+
26+
import com.puppycrawl.tools.checkstyle.Checker;
27+
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
28+
import com.puppycrawl.tools.checkstyle.TreeWalker;
29+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
30+
import com.puppycrawl.tools.checkstyle.api.Configuration;
31+
32+
/**
33+
* Processes the specific file using our custom check.
34+
*
35+
* <p>This utility class would run a custom check on the file with the given path and return
36+
* the collected property info.</p>
37+
* @author LuoLiangchen
38+
*/
39+
public final class CustomCheckProcessor {
40+
/** Prevents instantiation. */
41+
private CustomCheckProcessor() {
42+
}
43+
44+
/**
45+
* Processes the file with the given path using the given custom check.
46+
*
47+
* <p>The custom check needs a public/static function to retrieve the processing result.</p>
48+
* @param path the path of the file
49+
* @param checkClass the class of the custom check
50+
* @throws CheckstyleException failure when running the check
51+
*/
52+
public static void process(String path, Class<?> checkClass) throws CheckstyleException {
53+
final DefaultConfiguration moduleConfig = createModuleConfig(checkClass);
54+
final Configuration dc = createTreeWalkerConfig(moduleConfig);
55+
final Checker checker = new Checker();
56+
checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
57+
checker.configure(dc);
58+
final List<File> processedFiles = Collections.singletonList(new File(path));
59+
checker.process(processedFiles);
60+
}
61+
62+
/**
63+
* Creates {@link DefaultConfiguration} for the {@link TreeWalker}
64+
* based on the given {@link Configuration} instance.
65+
* @param config {@link Configuration} instance.
66+
* @return {@link DefaultConfiguration} for the {@link TreeWalker}
67+
* based on the given {@link Configuration} instance.
68+
*/
69+
private static DefaultConfiguration createTreeWalkerConfig(Configuration config) {
70+
final DefaultConfiguration dc =
71+
new DefaultConfiguration("configuration");
72+
final DefaultConfiguration twConf = createModuleConfig(TreeWalker.class);
73+
// make sure that the tests always run with this charset
74+
dc.addAttribute("charset", "UTF-8");
75+
dc.addChild(twConf);
76+
twConf.addChild(config);
77+
return dc;
78+
}
79+
80+
/**
81+
* Creates {@link DefaultConfiguration} for the given class.
82+
* @param clazz the class of module
83+
* @return the {@link DefaultConfiguration} of the module class
84+
*/
85+
private static DefaultConfiguration createModuleConfig(Class<?> clazz) {
86+
return new DefaultConfiguration(clazz.getName());
87+
}
88+
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle: Checks Java source code for adherence to a set of rules.
3+
// Copyright (C) 2001-2017 the original author or authors.
4+
//
5+
// This library is free software; you can redistribute it and/or
6+
// modify it under the terms of the GNU Lesser General Public
7+
// License as published by the Free Software Foundation; either
8+
// version 2.1 of the License, or (at your option) any later version.
9+
//
10+
// This library is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
// Lesser General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU Lesser General Public
16+
// License along with this library; if not, write to the Free Software
17+
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package com.github.checkstyle.regression.customcheck;
21+
22+
import java.util.Collections;
23+
import java.util.LinkedHashMap;
24+
import java.util.LinkedHashSet;
25+
import java.util.LinkedList;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Optional;
29+
import java.util.Set;
30+
import java.util.regex.Matcher;
31+
import java.util.regex.Pattern;
32+
33+
import com.github.checkstyle.regression.data.ImmutableProperty;
34+
import com.github.checkstyle.regression.data.ModuleInfo;
35+
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
36+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
38+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
39+
40+
/**
41+
* The custom check which processes the unit test class of a checkstyle module and grab the
42+
* possible properties that could be used for generating config.
43+
*
44+
* <p>The check would walk through the {@code @Test} annotation, find variable definition
45+
* like {@code final DefaultConfiguration checkConfig = createModuleConfig(FooCheck.class)}
46+
* and grab the property info from {@link DefaultConfiguration#addAttribute(String, String)}
47+
* method call.</p>
48+
*
49+
* <p>The check also support to detect module config which defined as a class field. This kind
50+
* of config is detected by the assignment in the unit test method, like {@code checkConfig =
51+
* createModuleConfig(FooCheck.class)}, where {@code checkConfig} could be a class field.</p>
52+
* @author LuoLiangchen
53+
*/
54+
public class UnitTestProcessorCheck extends AbstractCheck {
55+
/** The map of unit test method name to properties. */
56+
private static final Map<String, Set<ModuleInfo.Property>> UNIT_TEST_TO_PROPERTIES =
57+
new LinkedHashMap<>();
58+
59+
@Override
60+
public int[] getDefaultTokens() {
61+
return new int[] {
62+
TokenTypes.ANNOTATION,
63+
};
64+
}
65+
66+
@Override
67+
public int[] getRequiredTokens() {
68+
return new int[] {
69+
TokenTypes.ANNOTATION,
70+
};
71+
}
72+
73+
@Override
74+
public int[] getAcceptableTokens() {
75+
return new int[] {
76+
TokenTypes.ANNOTATION,
77+
};
78+
}
79+
80+
@Override
81+
public void visitToken(DetailAST ast) {
82+
if ("Test".equals(ast.findFirstToken(TokenTypes.IDENT).getText())) {
83+
final DetailAST methodDef = ast.getParent().getParent();
84+
final DetailAST methodBlock = methodDef.findFirstToken(TokenTypes.SLIST);
85+
final Optional<String> configVariableName =
86+
getModuleConfigVariableName(methodBlock);
87+
if (configVariableName.isPresent()) {
88+
final Set<ModuleInfo.Property> properties = new LinkedHashSet<>();
89+
90+
for (DetailAST expr : getAllChildrenWithToken(methodBlock, TokenTypes.EXPR)) {
91+
if (isAddAttributeMethodCall(expr.getFirstChild(), configVariableName.get())) {
92+
final DetailAST elist =
93+
expr.getFirstChild().findFirstToken(TokenTypes.ELIST);
94+
final String key =
95+
convertExpressionToText(elist.getFirstChild().getFirstChild());
96+
final String value =
97+
convertExpressionToText(elist.getLastChild().getFirstChild());
98+
properties.add(ImmutableProperty.builder().name(key).value(value).build());
99+
}
100+
}
101+
102+
if (!UNIT_TEST_TO_PROPERTIES.containsValue(properties)) {
103+
final String methodName = methodDef.findFirstToken(TokenTypes.IDENT).getText();
104+
UNIT_TEST_TO_PROPERTIES.put(methodName, properties);
105+
}
106+
}
107+
}
108+
}
109+
110+
/**
111+
* Clears the map of unit test method name to properties.
112+
*/
113+
public static void clearUnitTestToPropertiesMap() {
114+
UNIT_TEST_TO_PROPERTIES.clear();
115+
}
116+
117+
/**
118+
* Gets the map of unit test method name to properties.
119+
* @return the map of unit test method name to properties
120+
*/
121+
public static Map<String, Set<ModuleInfo.Property>> getUnitTestToPropertiesMap() {
122+
return Collections.unmodifiableMap(UNIT_TEST_TO_PROPERTIES);
123+
}
124+
125+
/**
126+
* Gets the module config variable name, if it exists.
127+
* @param methodBlock the UT method block ast, which should have a type {@link TokenTypes#SLIST}
128+
* @return the optional variable name, if it exists
129+
*/
130+
private static Optional<String> getModuleConfigVariableName(DetailAST methodBlock) {
131+
Optional<String> returnValue = Optional.empty();
132+
133+
for (DetailAST ast = methodBlock.getFirstChild(); ast != null; ast = ast.getNextSibling()) {
134+
if (ast.getType() == TokenTypes.VARIABLE_DEF) {
135+
final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
136+
final DetailAST assign = ast.findFirstToken(TokenTypes.ASSIGN);
137+
if (isDefaultConfigurationType(type) && isCreateModuleConfigAssign(assign)) {
138+
returnValue = Optional.of(type.getNextSibling().getText());
139+
}
140+
}
141+
else if (ast.getType() == TokenTypes.EXPR
142+
&& ast.getFirstChild().getType() == TokenTypes.ASSIGN) {
143+
final DetailAST exprChild = ast.getFirstChild();
144+
if (isCreateModuleConfigAssign(exprChild)) {
145+
returnValue = Optional.of(exprChild.getFirstChild().getText());
146+
}
147+
}
148+
if (returnValue.isPresent()) {
149+
break;
150+
}
151+
}
152+
153+
return returnValue;
154+
}
155+
156+
/**
157+
* Checks whether this {@link TokenTypes#TYPE} ast is {@link DefaultConfiguration}.
158+
* @param ast the {@link TokenTypes#TYPE} ast
159+
* @return true if the type is {@link DefaultConfiguration}
160+
*/
161+
private static boolean isDefaultConfigurationType(DetailAST ast) {
162+
return "DefaultConfiguration".equals(ast.getFirstChild().getText());
163+
}
164+
165+
/**
166+
* Checks whether this {@link TokenTypes#ASSIGN} ast contains
167+
* a {@code createModuleConfig} method call.
168+
* @param ast the {@link TokenTypes#ASSIGN} ast
169+
* @return true if the assignment contains a {@code createModuleConfig} method call
170+
*/
171+
private static boolean isCreateModuleConfigAssign(DetailAST ast) {
172+
final boolean result;
173+
174+
if (ast == null) {
175+
result = false;
176+
}
177+
else {
178+
final DetailAST methodCall;
179+
if (ast.findFirstToken(TokenTypes.METHOD_CALL) == null) {
180+
methodCall = ast.getFirstChild().getFirstChild();
181+
}
182+
else {
183+
methodCall = ast.findFirstToken(TokenTypes.METHOD_CALL);
184+
}
185+
result = methodCall.getType() == TokenTypes.METHOD_CALL
186+
&& methodCall.getFirstChild().getType() == TokenTypes.IDENT
187+
&& "createModuleConfig".equals(methodCall.getFirstChild().getText());
188+
}
189+
190+
return result;
191+
}
192+
193+
/**
194+
* Gets all children of a ast with the given tokens type.
195+
* @param parent the parent ast
196+
* @param type the given tokens type
197+
* @return the children with the given tokens type
198+
*/
199+
private static List<DetailAST> getAllChildrenWithToken(DetailAST parent, int type) {
200+
final List<DetailAST> returnValue = new LinkedList<>();
201+
202+
for (DetailAST ast = parent.getFirstChild(); ast != null; ast = ast.getNextSibling()) {
203+
if (ast.getType() == type) {
204+
returnValue.add(ast);
205+
}
206+
}
207+
208+
return returnValue;
209+
}
210+
211+
/**
212+
* Checks whether this expression is an {@code addAttribute} method call on an instance with
213+
* the given variable name.
214+
* @param ast the ast to check
215+
* @param variableName the given variable name of the module config instance
216+
* @return true if the expression is a valid {@code addAttribute} method call
217+
*/
218+
private static boolean isAddAttributeMethodCall(DetailAST ast, String variableName) {
219+
final boolean result;
220+
221+
if (ast.getType() == TokenTypes.METHOD_CALL
222+
&& ast.getFirstChild().getType() == TokenTypes.DOT) {
223+
final DetailAST dot = ast.getFirstChild();
224+
result = variableName.equals(dot.getFirstChild().getText())
225+
&& "addAttribute".equals(dot.getLastChild().getText());
226+
}
227+
else {
228+
result = false;
229+
}
230+
231+
return result;
232+
}
233+
234+
/**
235+
* Converts an expression content to raw text.
236+
* @param ast the first child of expression ast to convert
237+
* @return the converted raw text
238+
*/
239+
private String convertExpressionToText(DetailAST ast) {
240+
String result = null;
241+
if (ast.getType() == TokenTypes.STRING_LITERAL) {
242+
final String original = ast.getText();
243+
result = original.substring(1, original.length() - 1);
244+
}
245+
else if (ast.getType() == TokenTypes.PLUS) {
246+
result = convertExpressionToText(ast.getFirstChild())
247+
+ convertExpressionToText(ast.getLastChild());
248+
}
249+
else if (ast.getType() == TokenTypes.LITERAL_NULL) {
250+
result = ast.getText();
251+
}
252+
else if (ast.getType() == TokenTypes.METHOD_CALL) {
253+
final String line = getFileContents().getLine(ast.getLineNo() - 1);
254+
final Pattern pattern = Pattern.compile(
255+
"\\.addAttribute\\(.+, (?:.+\\.)*(.+)\\.toString\\(\\)\\);");
256+
final Matcher matcher = pattern.matcher(line);
257+
if (matcher.find()) {
258+
result = matcher.group(1);
259+
}
260+
}
261+
262+
if (result == null) {
263+
throw new IllegalStateException("the processor cannot support this ast: " + ast);
264+
}
265+
266+
return result;
267+
}
268+
}

0 commit comments

Comments
 (0)