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.invokeVoidStaticMethod(JavaAstVisitor.class, "addLastSibling",
243                     null, null);
244         }, "Method should not throw exception.");
245     }
246     /**
247      * This test exists to kill surviving mutation from pitest removing expression AST building
248      * optimization in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}.
249      * We do not use {@link JavaParser#parse(FileContents)} here due to DFA clearing hack.
250      *
251      * <p>
252      * Reason: we have iterative expression AST building to avoid stackoverflow
253      * in {@link JavaAstVisitor#visitBinOp(JavaLanguageParser.BinOpContext)}. In actual
254      * generated parser, we avoid stackoverflow thanks to the left recursive expression
255      * rule (eliminating unnecessary recursive calls to hierarchical expression production rules).
256      * However, ANTLR's ParserATNSimulator has no such optimization. So, the number of recursive
257      * calls to ParserATNSimulator#closure when calling ParserATNSimulator#clearDFA causes a
258      * StackOverflow error. We avoid this by using the single argument constructor (thus not
259      * forcing DFA clearing) in this test.
260      * </p>
261      *
262      * @throws Exception if input file does not exist
263      */
264 
265     @Test
266     public void testNoStackOverflowOnDeepStringConcat() throws Exception {
267         final File file =
268                 new File(getPath("InputJavaAstVisitorNoStackOverflowOnDeepStringConcat.java"));
269         final FileText fileText = new FileText(file, StandardCharsets.UTF_8.name());
270         final FileContents contents = new FileContents(fileText);
271 
272         final String fullText = contents.getText().getFullText().toString();
273         final CharStream codePointCharStream = CharStreams.fromString(fullText);
274         final JavaLanguageLexer lexer = new JavaLanguageLexer(codePointCharStream, true);
275         lexer.setCommentListener(contents);
276 
277         final CommonTokenStream tokenStream = new CommonTokenStream(lexer);
278         final JavaLanguageParser parser = new JavaLanguageParser(tokenStream);
279 
280         final JavaLanguageParser.CompilationUnitContext compilationUnit = parser.compilationUnit();
281 
282         // We restrict execution to use limited resources here, so that we can
283         // kill surviving pitest mutation from removal of nested binary operation
284         // optimization in JavaAstVisitor#visitBinOp. Limited resources (small stack size)
285         // ensure that we throw a StackOverflowError if optimization is removed.
286         final DetailAST root = TestUtil.getResultWithLimitedResources(
287                 () -> new JavaAstVisitor(tokenStream).visit(compilationUnit)
288         );
289 
290         assertWithMessage("File parsing and AST building should complete successfully.")
291                 .that(root)
292                 .isNotNull();
293     }
294 }