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              "visitLiteralPrimary",
85              "visitPatternDefinition",
86              "visitLocalType",
87              "visitLocalTypeDeclaration",
88              "visitRecordBodyDeclaration",
89              "visitResource",
90              "visitVariableInitializer",
91              "visitLambdaBody",
92              "visitPatternVariableDef",
93  
94              // AST built in parent rule
95              "visitCreatedNameExtended",
96              "visitSuperSuffixSimple",
97              "visitFieldAccessNoIdent",
98              "visitClassType",
99              "visitClassOrInterfaceTypeExtended",
100             "visitQualifiedNameExtended",
101             "visitGuard"
102     );
103 
104     @Override
105     public String getPackageLocation() {
106         return "com/puppycrawl/tools/checkstyle/javaastvisitor";
107     }
108 
109     @Test
110     public void testAllVisitMethodsAreOverridden() {
111         final Method[] baseVisitMethods = JavaLanguageParserBaseVisitor
112                 .class.getDeclaredMethods();
113         final Method[] visitMethods = JavaAstVisitor.class.getDeclaredMethods();
114 
115         final Set<String> filteredBaseVisitMethodNames = Arrays.stream(baseVisitMethods)
116                 .filter(method -> !VISIT_METHODS_NOT_OVERRIDDEN.contains(method.getName()))
117                 .filter(method -> method.getName().contains("visit"))
118                 .filter(method -> method.getModifiers() == Modifier.PUBLIC)
119                 .map(Method::getName)
120                 .collect(Collectors.toUnmodifiableSet());
121 
122         final Set<String> filteredVisitMethodNames = Arrays.stream(visitMethods)
123                 .filter(method -> method.getName().contains("visit"))
124                 // remove overridden 'visit' method from ParseTreeVisitor interface in
125                 // JavaAstVisitor
126                 .filter(method -> !"visit".equals(method.getName()))
127                 .filter(method -> method.getModifiers() == Modifier.PUBLIC)
128                 .map(Method::getName)
129                 .collect(Collectors.toUnmodifiableSet());
130 
131         assertWithMessage(
132             "Visit methods in 'JavaLanguageParserBaseVisitor' generated from production "
133                 + "rules and labeled alternatives in 'JavaLanguageParser.g4' should be "
134                 + "overridden in 'JavaAstVisitor' or be added to 'VISIT_METHODS_NOT_OVERRIDDEN' "
135                         + "with comment explaining why.")
136                 .that(filteredVisitMethodNames)
137                 .containsExactlyElementsIn(filteredBaseVisitMethodNames);
138     }
139 
140     @Test
141     public void testOrderOfVisitMethodsAndProductionRules() throws Exception {
142         // Order of BaseVisitor's generated 'visit' methods match the order of
143         // production rules in 'JavaLanguageParser.g4'.
144         final String baseVisitorFilename = "target/generated-sources/antlr/com/puppycrawl"
145                 + "/tools/checkstyle/grammar/java/JavaLanguageParserBaseVisitor.java";
146         final DetailAST baseVisitorAst = JavaParser.parseFile(new File(baseVisitorFilename),
147                             JavaParser.Options.WITHOUT_COMMENTS);
148 
149         final String visitorFilename = "src/main/java/com/puppycrawl/tools/checkstyle"
150                 + "/JavaAstVisitor.java";
151         final DetailAST visitorAst = JavaParser.parseFile(new File(visitorFilename),
152                             JavaParser.Options.WITHOUT_COMMENTS);
153 
154         final List<String> orderedBaseVisitorMethodNames =
155                 getOrderedVisitMethodNames(baseVisitorAst);
156         final List<String> orderedVisitorMethodNames =
157                 getOrderedVisitMethodNames(visitorAst);
158 
159         orderedBaseVisitorMethodNames.removeAll(VISIT_METHODS_NOT_OVERRIDDEN);
160 
161         // remove overridden 'visit' method from ParseTreeVisitor interface in JavaAstVisitor
162         orderedVisitorMethodNames.remove("visit");
163 
164         assertWithMessage("Visit methods in 'JavaAstVisitor' should appear in same order as "
165                 + "production rules and labeled alternatives in 'JavaLanguageParser.g4'.")
166                 .that(orderedVisitorMethodNames)
167                 .containsExactlyElementsIn(orderedBaseVisitorMethodNames)
168                 .inOrder();
169     }
170 
171     /**
172      * The reason we have this test is that we forgot to add the imaginary 'EXPR' node
173      * to a production rule in the parser grammar (we should have used 'expression'
174      * instead of 'expr'). This test is a reminder to question the usage of the 'expr' parser
175      * rule in the parser grammar when we update the count to make sure we are not missing
176      * an imaginary 'EXPR' node in the AST.
177      *
178      * @throws IOException if file does not exist
179      */
180     @Test
181     public void countExprUsagesInParserGrammar() throws IOException {
182         final String parserGrammarFilename = "src/main/resources/com/puppycrawl"
183                 + "/tools/checkstyle/grammar/java/JavaLanguageParser.g4";
184 
185         final int actualExprCount = Arrays.stream(new FileText(new File(parserGrammarFilename),
186                         StandardCharsets.UTF_8.name()).toLinesArray())
187                 .mapToInt(JavaAstVisitorTest::countExprInLine)
188                 .sum();
189 
190         // Any time we update this count, we should question why we are not building an imaginary
191         // 'EXPR' node.
192         final int expectedExprCount = 44;
193 
194         assertWithMessage("The 'expr' parser rule does not build an imaginary"
195                 + " 'EXPR' node. Any usage of this rule should be questioned.")
196                 .that(actualExprCount)
197                 .isEqualTo(expectedExprCount);
198 
199     }
200 
201     private static int countExprInLine(String line) {
202         return (int) Arrays.stream(line.split(" "))
203                 .filter("expr"::equals)
204                 .count();
205     }
206 
207     /**
208      * Finds all {@code visit...} methods in a source file, and collects
209      * the method names into a list. This method counts on the simple structure
210      * of 'JavaAstVisitor' and 'JavaLanguageParserBaseVisitor'.
211      *
212      * @param root the root of the AST to extract method names from
213      * @return list of all {@code visit...} method names
214      */
215     private static List<String> getOrderedVisitMethodNames(DetailAST root) {
216         final List<String> orderedVisitMethodNames = new ArrayList<>();
217 
218         DetailAST classDef = root.getFirstChild();
219         while (classDef.getType() != TokenTypes.CLASS_DEF) {
220             classDef = classDef.getNextSibling();
221         }
222 
223         final DetailAST objBlock = classDef.findFirstToken(TokenTypes.OBJBLOCK);
224         DetailAST objBlockChild = objBlock.findFirstToken(TokenTypes.METHOD_DEF);
225         while (objBlockChild != null) {
226             if (isVisitMethod(objBlockChild)) {
227                 orderedVisitMethodNames.add(objBlockChild
228                         .findFirstToken(TokenTypes.IDENT)
229                         .getText());
230             }
231             objBlockChild = objBlockChild.getNextSibling();
232         }
233         return orderedVisitMethodNames;
234     }
235 
236     /**
237      * Checks if given AST is a visit method.
238      *
239      * @param objBlockChild AST to check
240      * @return true if AST is a visit method
241      */
242     private static boolean isVisitMethod(DetailAST objBlockChild) {
243         return objBlockChild.getType() == TokenTypes.METHOD_DEF
244                 && objBlockChild.findFirstToken(TokenTypes.IDENT).getText().contains("visit");
245     }
246 
247     @Test
248     public void testNullSelfInAddLastSibling() {
249         assertDoesNotThrow(() -> {
250             TestUtil.invokeVoidStaticMethod(JavaAstVisitor.class, "addLastSibling",
251                     null, null);
252         }, "Method should not throw exception.");
253     }
254     /**
255      * This test exists to kill surviving mutation from pitest removing expression AST building
256      * optimization in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}.
257      * We do not use {@link JavaParser#parse(FileContents)} here due to DFA clearing hack.
258      *
259      * <p>
260      * Reason: we have iterative expression AST building to avoid stackoverflow
261      * in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}. In actual
262      * generated parser, we avoid stackoverflow thanks to the left recursive expression
263      * rule (eliminating unnecessary recursive calls to hierarchical expression production rules).
264      * However, ANTLR's ParserATNSimulator has no such optimization. So, the number of recursive
265      * calls to ParserATNSimulator#closure when calling ParserATNSimulator#clearDFA causes a
266      * StackOverflow error. We avoid this by using the single argument constructor (thus not
267      * forcing DFA clearing) in this test.
268      * </p>
269      *
270      * @throws Exception if input file does not exist
271      */
272 
273     @Test
274     public void testNoStackOverflowOnDeepStringConcat() throws Exception {
275         final File file =
276                 new File(getPath("InputJavaAstVisitorNoStackOverflowOnDeepStringConcat.java"));
277         final FileText fileText = new FileText(file, StandardCharsets.UTF_8.name());
278         final FileContents contents = new FileContents(fileText);
279 
280         final String fullText = contents.getText().getFullText().toString();
281         final CharStream codePointCharStream = CharStreams.fromString(fullText);
282         final JavaLanguageLexer lexer = new JavaLanguageLexer(codePointCharStream, true);
283         lexer.setCommentListener(contents);
284 
285         final CommonTokenStream tokenStream = new CommonTokenStream(lexer);
286         final JavaLanguageParser parser = new JavaLanguageParser(tokenStream);
287 
288         final JavaLanguageParser.CompilationUnitContext compilationUnit = parser.compilationUnit();
289 
290         // We restrict execution to use limited resources here, so that we can
291         // kill surviving pitest mutation from removal of nested binary operation
292         // optimization in JavaAstVisitor#visitBinOp. Limited resources (small stack size)
293         // ensure that we throw a StackOverflowError if optimization is removed.
294         final DetailAST root = TestUtil.getResultWithLimitedResources(
295                 () -> new JavaAstVisitor(tokenStream).visit(compilationUnit)
296         );
297 
298         assertWithMessage("File parsing and AST building should complete successfully.")
299                 .that(root)
300                 .isNotNull();
301     }
302 }