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