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