View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.puppycrawl.tools.checkstyle;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.lang.reflect.Method;
28  import java.lang.reflect.Modifier;
29  import java.nio.charset.StandardCharsets;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.List;
33  import java.util.Set;
34  import java.util.stream.Collectors;
35  
36  import org.antlr.v4.runtime.CharStream;
37  import org.antlr.v4.runtime.CharStreams;
38  import org.antlr.v4.runtime.CommonTokenStream;
39  import org.junit.jupiter.api.Test;
40  
41  import com.puppycrawl.tools.checkstyle.api.DetailAST;
42  import com.puppycrawl.tools.checkstyle.api.FileContents;
43  import com.puppycrawl.tools.checkstyle.api.FileText;
44  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
45  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageLexer;
46  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageParser;
47  import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageParserBaseVisitor;
48  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
49  
50  // -@cs[ClassDataAbstractionCoupling] No way to split up class usage.
51  
52  /**
53   * Tests for {@code JavaAstVisitor}.
54   *
55   * <p>Verifies correct traversal and processing of Java AST nodes.</p>
56   *
57   * @noinspection JavadocReference References are valid in project context
58   * @noinspectionreason JavadocReference - References are valid
59   */
60  public class JavaAstVisitorTest extends AbstractModuleTestSupport {
61  
62      /**
63       * If a visit method is not overridden, we should explain why we do not 'visit' the
64       * parse tree at this node and construct an AST. Reasons could include that we have
65       * no terminal symbols (tokens) in the corresponding production rule, or that
66       * we handle the construction of this particular AST in its parent node. If we
67       * have a production rule where we have terminal symbols (tokens), but we do not build
68       * an AST from tokens in the rule context, the rule is extraneous.
69       */
70      private static final Set<String> VISIT_METHODS_NOT_OVERRIDDEN = Set.of(
71              // no tokens in production rule, so no AST to build
72              "visitClassOrInterfaceOrPrimitiveType",
73              "visitNonWildcardTypeArgs",
74              "visitStat",
75              "visitAnnotationConstantRest",
76              "visitSwitchLabeledRule",
77              "visitLocalVariableDeclaration",
78              "visitTypes",
79              "visitSwitchStat",
80              "visitSwitchPrimary",
81              "visitClassDef",
82              "visitInterfaceMemberDeclaration",
83              "visitMemberDeclaration",
84              "visitCompactMember",
85              "visitCompactMemberDeclaration",
86              "visitLiteralPrimary",
87              "visitPatternDefinition",
88              "visitLocalType",
89              "visitLocalTypeDeclaration",
90              "visitRecordBodyDeclaration",
91              "visitResource",
92              "visitVariableInitializer",
93              "visitLambdaBody",
94              "visitPatternVariableDef",
95              "visitConstructorBlockStatement",
96  
97              // AST built in parent rule
98              "visitCreatedNameExtended",
99              "visitSuperSuffixSimple",
100             "visitFieldAccessNoIdent",
101             "visitClassType",
102             "visitClassOrInterfaceTypeExtended",
103             "visitQualifiedNameExtended",
104             "visitGuard"
105     );
106 
107     @Override
108     public String getPackageLocation() {
109         return "com/puppycrawl/tools/checkstyle/javaastvisitor";
110     }
111 
112     @Test
113     public void testAllVisitMethodsAreOverridden() {
114         final Method[] baseVisitMethods = JavaLanguageParserBaseVisitor
115                 .class.getDeclaredMethods();
116         final Method[] visitMethods = JavaAstVisitor.class.getDeclaredMethods();
117 
118         final Set<String> filteredBaseVisitMethodNames = Arrays.stream(baseVisitMethods)
119                 .filter(method -> !VISIT_METHODS_NOT_OVERRIDDEN.contains(method.getName()))
120                 .filter(method -> method.getName().contains("visit"))
121                 .filter(method -> method.getModifiers() == Modifier.PUBLIC)
122                 .map(Method::getName)
123                 .collect(Collectors.toUnmodifiableSet());
124 
125         final Set<String> filteredVisitMethodNames = Arrays.stream(visitMethods)
126                 .filter(method -> method.getName().contains("visit"))
127                 // remove overridden 'visit' method from ParseTreeVisitor interface in
128                 // JavaAstVisitor
129                 .filter(method -> !"visit".equals(method.getName()))
130                 .filter(method -> method.getModifiers() == Modifier.PUBLIC)
131                 .map(Method::getName)
132                 .collect(Collectors.toUnmodifiableSet());
133 
134         assertWithMessage(
135             "Visit methods in 'JavaLanguageParserBaseVisitor' generated from production "
136                 + "rules and labeled alternatives in 'JavaLanguageParser.g4' should be "
137                 + "overridden in 'JavaAstVisitor' or be added to 'VISIT_METHODS_NOT_OVERRIDDEN' "
138                         + "with comment explaining why.")
139                 .that(filteredVisitMethodNames)
140                 .containsExactlyElementsIn(filteredBaseVisitMethodNames);
141     }
142 
143     @Test
144     public void testOrderOfVisitMethodsAndProductionRules() throws Exception {
145         // Order of BaseVisitor's generated 'visit' methods match the order of
146         // production rules in 'JavaLanguageParser.g4'.
147         final String baseVisitorFilename = "target/generated-sources/antlr/com/puppycrawl"
148                 + "/tools/checkstyle/grammar/java/JavaLanguageParserBaseVisitor.java";
149         final DetailAST baseVisitorAst = JavaParser.parseFile(new File(baseVisitorFilename),
150                             JavaParser.Options.WITHOUT_COMMENTS);
151 
152         final String visitorFilename = "src/main/java/com/puppycrawl/tools/checkstyle"
153                 + "/JavaAstVisitor.java";
154         final DetailAST visitorAst = JavaParser.parseFile(new File(visitorFilename),
155                             JavaParser.Options.WITHOUT_COMMENTS);
156 
157         final List<String> orderedBaseVisitorMethodNames =
158                 getOrderedVisitMethodNames(baseVisitorAst);
159         final List<String> orderedVisitorMethodNames =
160                 getOrderedVisitMethodNames(visitorAst);
161 
162         orderedBaseVisitorMethodNames.removeAll(VISIT_METHODS_NOT_OVERRIDDEN);
163 
164         // remove overridden 'visit' method from ParseTreeVisitor interface in JavaAstVisitor
165         orderedVisitorMethodNames.remove("visit");
166 
167         assertWithMessage("Visit methods in 'JavaAstVisitor' should appear in same order as "
168                 + "production rules and labeled alternatives in 'JavaLanguageParser.g4'.")
169                 .that(orderedVisitorMethodNames)
170                 .containsExactlyElementsIn(orderedBaseVisitorMethodNames)
171                 .inOrder();
172     }
173 
174     /**
175      * The reason we have this test is that we forgot to add the imaginary 'EXPR' node
176      * to a production rule in the parser grammar (we should have used 'expression'
177      * instead of 'expr'). This test is a reminder to question the usage of the 'expr' parser
178      * rule in the parser grammar when we update the count to make sure we are not missing
179      * an imaginary 'EXPR' node in the AST.
180      *
181      * @throws IOException if file does not exist
182      */
183     @Test
184     public void countExprUsagesInParserGrammar() throws IOException {
185         final String parserGrammarFilename = "src/main/resources/com/puppycrawl"
186                 + "/tools/checkstyle/grammar/java/JavaLanguageParser.g4";
187 
188         final int actualExprCount = Arrays.stream(new FileText(new File(parserGrammarFilename),
189                         StandardCharsets.UTF_8.name()).toLinesArray())
190                 .mapToInt(JavaAstVisitorTest::countExprInLine)
191                 .sum();
192 
193         // Any time we update this count, we should question why we are not building an imaginary
194         // 'EXPR' node.
195         final int expectedExprCount = 44;
196 
197         assertWithMessage("The 'expr' parser rule does not build an imaginary"
198                 + " 'EXPR' node. Any usage of this rule should be questioned.")
199                 .that(actualExprCount)
200                 .isEqualTo(expectedExprCount);
201 
202     }
203 
204     private static int countExprInLine(String line) {
205         return (int) Arrays.stream(line.split(" "))
206                 .filter("expr"::equals)
207                 .count();
208     }
209 
210     /**
211      * Finds all {@code visit...} methods in a source file, and collects
212      * the method names into a list. This method counts on the simple structure
213      * of 'JavaAstVisitor' and 'JavaLanguageParserBaseVisitor'.
214      *
215      * @param root the root of the AST to extract method names from
216      * @return list of all {@code visit...} method names
217      */
218     private static List<String> getOrderedVisitMethodNames(DetailAST root) {
219         final List<String> orderedVisitMethodNames = new ArrayList<>();
220 
221         DetailAST classDef = root.getFirstChild();
222         while (classDef.getType() != TokenTypes.CLASS_DEF) {
223             classDef = classDef.getNextSibling();
224         }
225 
226         final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
227         DetailAST objBlockChild = objBlock.findFirstToken(TokenTypes.METHOD_DEF);
228         while (objBlockChild != null) {
229             if (isVisitMethod(objBlockChild)) {
230                 orderedVisitMethodNames.add(objBlockChild
231                         .findFirstToken(TokenTypes.IDENT)
232                         .getText());
233             }
234             objBlockChild = objBlockChild.getNextSibling();
235         }
236         return orderedVisitMethodNames;
237     }
238 
239     /**
240      * Checks if given AST is a visit method.
241      *
242      * @param objBlockChild AST to check
243      * @return true if AST is a visit method
244      */
245     private static boolean isVisitMethod(DetailAST objBlockChild) {
246         return objBlockChild.getType() == TokenTypes.METHOD_DEF
247                 && objBlockChild.findFirstToken(TokenTypes.IDENT).getText().contains("visit");
248     }
249 
250     @Test
251     public void testNullSelfInAddLastSibling() {
252         assertDoesNotThrow(() -> {
253             TestUtil.invokeVoidStaticMethod(JavaAstVisitor.class, "addLastSibling",
254                     null, null);
255         }, "Method should not throw exception.");
256     }
257     /**
258      * This test exists to kill surviving mutation from pitest removing expression AST building
259      * optimization in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}.
260      * We do not use {@link JavaParser#parse(FileContents)} here due to DFA clearing hack.
261      *
262      * <p>
263      * Reason: we have iterative expression AST building to avoid stackoverflow
264      * in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}. In actual
265      * generated parser, we avoid stackoverflow thanks to the left recursive expression
266      * rule (eliminating unnecessary recursive calls to hierarchical expression production rules).
267      * However, ANTLR's ParserATNSimulator has no such optimization. So, the number of recursive
268      * calls to ParserATNSimulator#closure when calling ParserATNSimulator#clearDFA causes a
269      * StackOverflow error. We avoid this by using the single argument constructor (thus not
270      * forcing DFA clearing) in this test.
271      * </p>
272      *
273      * @throws Exception if input file does not exist
274      */
275 
276     @Test
277     public void testNoStackOverflowOnDeepStringConcat() throws Exception {
278         final File file =
279                 new File(getPath("InputJavaAstVisitorNoStackOverflowOnDeepStringConcat.java"));
280         final FileText fileText = new FileText(file, StandardCharsets.UTF_8.name());
281         final FileContents contents = new FileContents(fileText);
282 
283         final String fullText = contents.getText().getFullText().toString();
284         final CharStream codePointCharStream = CharStreams.fromString(fullText);
285         final JavaLanguageLexer lexer = new JavaLanguageLexer(codePointCharStream, true);
286         lexer.setCommentListener(contents);
287 
288         final CommonTokenStream tokenStream = new CommonTokenStream(lexer);
289         final JavaLanguageParser parser = new JavaLanguageParser(tokenStream);
290 
291         final JavaLanguageParser.CompilationUnitContext compilationUnit = parser.compilationUnit();
292 
293         // We restrict execution to use limited resources here, so that we can
294         // kill surviving pitest mutation from removal of nested binary operation
295         // optimization in JavaAstVisitor#visitBinOp. Limited resources (small stack size)
296         // ensure that we throw a StackOverflowError if optimization is removed.
297         final DetailAST root = TestUtil.getResultWithLimitedResources(
298                 () -> new JavaAstVisitor(tokenStream).visit(compilationUnit)
299         );
300 
301         assertWithMessage("File parsing and AST building should complete successfully.")
302                 .that(root)
303                 .isNotNull();
304     }
305 }