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 java.io.File;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Comparator;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.SortedSet;
32  import java.util.TreeSet;
33  import java.util.stream.Collectors;
34  import java.util.stream.Stream;
35  
36  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
38  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
39  import com.puppycrawl.tools.checkstyle.api.Configuration;
40  import com.puppycrawl.tools.checkstyle.api.Context;
41  import com.puppycrawl.tools.checkstyle.api.DetailAST;
42  import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
43  import com.puppycrawl.tools.checkstyle.api.FileContents;
44  import com.puppycrawl.tools.checkstyle.api.FileText;
45  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
46  import com.puppycrawl.tools.checkstyle.api.Violation;
47  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
48  
49  /**
50   * Responsible for walking an abstract syntax tree and notifying interested
51   * checks at each node.
52   *
53   */
54  @FileStatefulCheck
55  public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
56  
57      /** Message to use when an exception occurs and should be printed as a violation. */
58      public static final String PARSE_EXCEPTION_MSG = "parse.exception";
59  
60      /** Maps from token name to ordinary checks. */
61      private final Map<Integer, Set<AbstractCheck>> tokenToOrdinaryChecks =
62          new HashMap<>();
63  
64      /** Maps from token name to comment checks. */
65      private final Map<Integer, Set<AbstractCheck>> tokenToCommentChecks =
66              new HashMap<>();
67  
68      /** Registered ordinary checks, that don't use comment nodes. */
69      private final Set<AbstractCheck> ordinaryChecks = createNewCheckSortedSet();
70  
71      /** Registered comment checks. */
72      private final Set<AbstractCheck> commentChecks = createNewCheckSortedSet();
73  
74      /** The ast filters. */
75      private final Set<TreeWalkerFilter> filters = new HashSet<>();
76  
77      /** The sorted set of violations. */
78      private final SortedSet<Violation> violations = new TreeSet<>();
79  
80      /** Context of child components. */
81      private Context childContext;
82  
83      /** A factory for creating submodules (i.e. the Checks) */
84      private ModuleFactory moduleFactory;
85  
86      /** Control whether to skip files with Java parsing exceptions. */
87      private boolean skipFileOnJavaParseException;
88  
89      /** Specify severity Level to log Java parsing exceptions when they are skipped. */
90      private SeverityLevel javaParseExceptionSeverity = SeverityLevel.ERROR;
91  
92      /**
93       * Creates a new {@code TreeWalker} instance.
94       */
95      public TreeWalker() {
96          setFileExtensions("java");
97      }
98  
99      /**
100      * Sets the module factory for creating child modules (Checks).
101      *
102      * @param moduleFactory the factory
103      */
104     public void setModuleFactory(ModuleFactory moduleFactory) {
105         this.moduleFactory = moduleFactory;
106     }
107 
108     /**
109      * Setter to control whether to skip files with Java parsing exceptions.
110      *
111      *  @param skipFileOnJavaParseException whether to skip files with Java parsing errors.
112      *  @since 10.18.0
113      */
114     public void setSkipFileOnJavaParseException(boolean skipFileOnJavaParseException) {
115         this.skipFileOnJavaParseException = skipFileOnJavaParseException;
116     }
117 
118     /**
119      * Setter to specify the severity level
120      * to log Java parsing exceptions when they are skipped.
121      *
122      *  @param javaParseExceptionSeverity severity level to log parsing exceptions
123      *      when they are skipped.
124      *  @since 10.18.0
125      */
126     public void setJavaParseExceptionSeverity(SeverityLevel javaParseExceptionSeverity) {
127         this.javaParseExceptionSeverity = javaParseExceptionSeverity;
128     }
129 
130     @Override
131     public void finishLocalSetup() {
132         final DefaultContext checkContext = new DefaultContext();
133         checkContext.add("severity", getSeverity());
134         checkContext.add("tabWidth", String.valueOf(getTabWidth()));
135         childContext = checkContext;
136     }
137 
138     /**
139      * {@inheritDoc} Creates child module.
140      *
141      * @noinspection ChainOfInstanceofChecks
142      * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
143      */
144     @Override
145     public void setupChild(Configuration childConf)
146             throws CheckstyleException {
147         final String name = childConf.getName();
148         final Object module;
149 
150         try {
151             module = moduleFactory.createModule(name);
152             if (module instanceof AbstractAutomaticBean bean) {
153                 bean.contextualize(childContext);
154                 bean.configure(childConf);
155             }
156         }
157         catch (final CheckstyleException exc) {
158             throw new CheckstyleException("cannot initialize module " + name
159                     + " - " + exc.getMessage(), exc);
160         }
161         if (module instanceof AbstractCheck check) {
162             check.init();
163             registerCheck(check);
164         }
165         else if (module instanceof TreeWalkerFilter filter) {
166             filters.add(filter);
167         }
168         else {
169             throw new CheckstyleException(
170                 "TreeWalker is not allowed as a parent of " + name
171                         + " Please review 'Parent Module' section for this Check in web"
172                         + " documentation if Check is standard.");
173         }
174     }
175 
176     /**
177      * {@inheritDoc} Processes the file.
178      *
179      * @noinspection ProhibitedExceptionThrown
180      * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
181      *     skipFileOnJavaParseException field
182      */
183     @Override
184     protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
185         // check if already checked and passed the file
186         if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
187             final FileContents contents = getFileContents();
188             DetailAST rootAST = null;
189             // whether skip the procedure after parsing Java files.
190             boolean skip = false;
191             try {
192                 rootAST = JavaParser.parse(contents);
193             }
194             // -@cs[IllegalCatch] There is no other way to obey skipFileOnJavaParseException field
195             catch (Exception exc) {
196                 if (!skipFileOnJavaParseException) {
197                     throw exc;
198                 }
199                 skip = true;
200                 violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG,
201                             new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null,
202                             getClass(), null));
203                 addViolations(violations);
204             }
205 
206             if (!skip) {
207                 if (!ordinaryChecks.isEmpty()) {
208                     walk(rootAST, contents, AstState.ORDINARY);
209                 }
210                 if (!commentChecks.isEmpty()) {
211                     final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
212                     walk(astWithComments, contents, AstState.WITH_COMMENTS);
213                 }
214                 if (filters.isEmpty()) {
215                     addViolations(violations);
216                 }
217                 else {
218                     final SortedSet<Violation> filteredViolations =
219                             getFilteredViolations(file.getAbsolutePath(), contents, rootAST);
220                     addViolations(filteredViolations);
221                 }
222             }
223             violations.clear();
224         }
225     }
226 
227     /**
228      * Returns filtered set of {@link Violation}.
229      *
230      * @param fileName path to the file
231      * @param fileContents the contents of the file
232      * @param rootAST root AST element {@link DetailAST} of the file
233      * @return filtered set of violations
234      */
235     private SortedSet<Violation> getFilteredViolations(
236             String fileName, FileContents fileContents, DetailAST rootAST) {
237         final SortedSet<Violation> result = new TreeSet<>(violations);
238         for (Violation element : violations) {
239             final TreeWalkerAuditEvent event =
240                     new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
241             for (TreeWalkerFilter filter : filters) {
242                 if (!filter.accept(event)) {
243                     result.remove(element);
244                     break;
245                 }
246             }
247         }
248         return result;
249     }
250 
251     /**
252      * Register a check for a given configuration.
253      *
254      * @param check the check to register
255      * @throws CheckstyleException if an error occurs
256      */
257     private void registerCheck(AbstractCheck check) throws CheckstyleException {
258         final int[] tokens;
259         final Set<String> checkTokens = check.getTokenNames();
260         if (checkTokens.isEmpty()) {
261             tokens = check.getDefaultTokens();
262         }
263         else {
264             tokens = check.getRequiredTokens();
265 
266             // register configured tokens
267             final int[] acceptableTokens = check.getAcceptableTokens();
268             Arrays.sort(acceptableTokens);
269             for (String token : checkTokens) {
270                 final int tokenId = TokenUtil.getTokenId(token);
271                 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
272                     registerCheck(tokenId, check);
273                 }
274                 else {
275                     final String message = String.format(Locale.ROOT, "Token \"%s\" was "
276                             + "not found in Acceptable tokens list in check %s",
277                             token, check.getClass().getName());
278                     throw new CheckstyleException(message);
279                 }
280             }
281         }
282         for (int element : tokens) {
283             registerCheck(element, check);
284         }
285         if (check.isCommentNodesRequired()) {
286             commentChecks.add(check);
287         }
288         else {
289             ordinaryChecks.add(check);
290         }
291     }
292 
293     /**
294      * Register a check for a specified token id.
295      *
296      * @param tokenId the id of the token
297      * @param check the check to register
298      * @throws CheckstyleException if Check is misconfigured
299      */
300     private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
301         if (check.isCommentNodesRequired()) {
302             tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
303                     .add(check);
304         }
305         else if (TokenUtil.isCommentType(tokenId)) {
306             final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
307                     + "token ('%s') and should override 'isCommentNodesRequired()' "
308                     + "method to return 'true'", check.getClass().getName(),
309                     TokenUtil.getTokenName(tokenId));
310             throw new CheckstyleException(message);
311         }
312         else {
313             tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
314                     .add(check);
315         }
316     }
317 
318     /**
319      * Initiates the walk of an AST.
320      *
321      * @param ast the root AST
322      * @param contents the contents of the file the AST was generated from.
323      * @param astState state of AST.
324      */
325     private void walk(DetailAST ast, FileContents contents,
326             AstState astState) {
327         notifyBegin(ast, contents, astState);
328         processIter(ast, astState);
329         notifyEnd(ast, astState);
330     }
331 
332     /**
333      * Notify checks that we are about to begin walking a tree.
334      *
335      * @param rootAST the root of the tree.
336      * @param contents the contents of the file the AST was generated from.
337      * @param astState state of AST.
338      */
339     private void notifyBegin(DetailAST rootAST, FileContents contents,
340             AstState astState) {
341         final Set<AbstractCheck> checks;
342 
343         if (astState == AstState.WITH_COMMENTS) {
344             checks = commentChecks;
345         }
346         else {
347             checks = ordinaryChecks;
348         }
349 
350         for (AbstractCheck check : checks) {
351             check.setFileContents(contents);
352             check.clearViolations();
353             check.beginTree(rootAST);
354         }
355     }
356 
357     /**
358      * Notify checks that we have finished walking a tree.
359      *
360      * @param rootAST the root of the tree.
361      * @param astState state of AST.
362      */
363     private void notifyEnd(DetailAST rootAST, AstState astState) {
364         final Set<AbstractCheck> checks;
365 
366         if (astState == AstState.WITH_COMMENTS) {
367             checks = commentChecks;
368         }
369         else {
370             checks = ordinaryChecks;
371         }
372 
373         for (AbstractCheck check : checks) {
374             check.finishTree(rootAST);
375             violations.addAll(check.getViolations());
376         }
377     }
378 
379     /**
380      * Notify checks that visiting a node.
381      *
382      * @param ast the node to notify for.
383      * @param astState state of AST.
384      */
385     private void notifyVisit(DetailAST ast, AstState astState) {
386         final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
387 
388         if (visitors != null) {
389             for (AbstractCheck check : visitors) {
390                 check.visitToken(ast);
391             }
392         }
393     }
394 
395     /**
396      * Notify checks that leaving a node.
397      *
398      * @param ast
399      *        the node to notify for
400      * @param astState state of AST.
401      */
402     private void notifyLeave(DetailAST ast, AstState astState) {
403         final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
404 
405         if (visitors != null) {
406             for (AbstractCheck check : visitors) {
407                 check.leaveToken(ast);
408             }
409         }
410     }
411 
412     /**
413      * Method returns list of checks.
414      *
415      * @param ast
416      *            the node to notify for
417      * @param astState
418      *            state of AST.
419      * @return list of visitors
420      */
421     private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
422         final Collection<AbstractCheck> visitors;
423         final int tokenId = ast.getType();
424 
425         if (astState == AstState.WITH_COMMENTS) {
426             visitors = tokenToCommentChecks.get(tokenId);
427         }
428         else {
429             visitors = tokenToOrdinaryChecks.get(tokenId);
430         }
431         return visitors;
432     }
433 
434     @Override
435     public void destroy() {
436         ordinaryChecks.forEach(AbstractCheck::destroy);
437         commentChecks.forEach(AbstractCheck::destroy);
438         super.destroy();
439     }
440 
441     @Override
442     public Set<String> getExternalResourceLocations() {
443         return Stream.concat(filters.stream(),
444                 Stream.concat(ordinaryChecks.stream(), commentChecks.stream()))
445             .filter(ExternalResourceHolder.class::isInstance)
446             .flatMap(resource -> {
447                 return ((ExternalResourceHolder) resource)
448                         .getExternalResourceLocations().stream();
449             })
450             .collect(Collectors.toUnmodifiableSet());
451     }
452 
453     /**
454      * Processes a node calling interested checks at each node.
455      * Uses iterative algorithm.
456      *
457      * @param root the root of tree for process
458      * @param astState state of AST.
459      */
460     private void processIter(DetailAST root, AstState astState) {
461         DetailAST curNode = root;
462         while (curNode != null) {
463             notifyVisit(curNode, astState);
464             DetailAST toVisit = curNode.getFirstChild();
465             while (curNode != null && toVisit == null) {
466                 notifyLeave(curNode, astState);
467                 toVisit = curNode.getNextSibling();
468                 curNode = curNode.getParent();
469             }
470             curNode = toVisit;
471         }
472     }
473 
474     /**
475      * Creates a new {@link SortedSet} with a deterministic order based on the
476      * Check's name before the default ordering.
477      *
478      * @return The new {@link SortedSet}.
479      */
480     private static SortedSet<AbstractCheck> createNewCheckSortedSet() {
481         return new TreeSet<>(
482                 Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName())
483                         .thenComparing(AbstractCheck::getId,
484                                 Comparator.nullsLast(Comparator.naturalOrder()))
485                         .thenComparingInt(AbstractCheck::hashCode));
486     }
487 
488     /**
489      * State of AST.
490      * Indicates whether tree contains certain nodes.
491      */
492     private enum AstState {
493 
494         /**
495          * Ordinary tree.
496          */
497         ORDINARY,
498 
499         /**
500          * AST contains comment nodes.
501          */
502         WITH_COMMENTS,
503 
504     }
505 
506 }