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 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     @Override
142     public void setupChild(Configuration childConf)
143             throws CheckstyleException {
144         final String name = childConf.getName();
145         final Object module;
146 
147         try {
148             module = moduleFactory.createModule(name);
149             if (module instanceof AbstractAutomaticBean bean) {
150                 bean.contextualize(childContext);
151                 bean.configure(childConf);
152             }
153         }
154         catch (final CheckstyleException exc) {
155             throw new CheckstyleException("cannot initialize module " + name, exc);
156         }
157         switch (module) {
158             case AbstractCheck check -> {
159                 check.init();
160                 registerCheck(check);
161             }
162             case TreeWalkerFilter filter -> filters.add(filter);
163             case null, default -> throw new CheckstyleException(
164                     "TreeWalker is not allowed as a parent of " + name
165                             + " Please review 'Parent Module' section for this Check in web"
166                             + " documentation if Check is standard.");
167         }
168     }
169 
170     /**
171      * {@inheritDoc} Processes the file.
172      *
173      * @noinspection ProhibitedExceptionThrown
174      * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
175      *     skipFileOnJavaParseException field
176      */
177     @Override
178     protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
179         // check if already checked and passed the file
180         if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
181             final FileContents contents = getFileContents();
182             DetailAST rootAST = null;
183             // whether skip the procedure after parsing Java files.
184             boolean skip = false;
185             try {
186                 rootAST = JavaParser.parse(contents);
187             }
188             // -@cs[IllegalCatch] There is no other way to obey skipFileOnJavaParseException field
189             catch (Exception exc) {
190                 if (!skipFileOnJavaParseException) {
191                     throw exc;
192                 }
193                 skip = true;
194                 violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG,
195                             new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null,
196                             getClass(), null));
197                 addViolations(violations);
198             }
199 
200             if (!skip) {
201                 if (!ordinaryChecks.isEmpty()) {
202                     walk(rootAST, contents, AstState.ORDINARY);
203                 }
204                 if (!commentChecks.isEmpty()) {
205                     final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
206                     walk(astWithComments, contents, AstState.WITH_COMMENTS);
207                 }
208                 if (filters.isEmpty()) {
209                     addViolations(violations);
210                 }
211                 else {
212                     final SortedSet<Violation> filteredViolations =
213                             getFilteredViolations(file.getAbsolutePath(), contents, rootAST);
214                     addViolations(filteredViolations);
215                 }
216             }
217             violations.clear();
218         }
219     }
220 
221     /**
222      * Returns filtered set of {@link Violation}.
223      *
224      * @param fileName path to the file
225      * @param fileContents the contents of the file
226      * @param rootAST root AST element {@link DetailAST} of the file
227      * @return filtered set of violations
228      */
229     private SortedSet<Violation> getFilteredViolations(
230             String fileName, FileContents fileContents, DetailAST rootAST) {
231         final SortedSet<Violation> result = new TreeSet<>(violations);
232         for (Violation element : violations) {
233             final TreeWalkerAuditEvent event =
234                     new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
235             for (TreeWalkerFilter filter : filters) {
236                 if (!filter.accept(event)) {
237                     result.remove(element);
238                     break;
239                 }
240             }
241         }
242         return result;
243     }
244 
245     /**
246      * Register a check for a given configuration.
247      *
248      * @param check the check to register
249      * @throws CheckstyleException if an error occurs
250      */
251     private void registerCheck(AbstractCheck check) throws CheckstyleException {
252         final int[] tokens;
253         final Set<String> checkTokens = check.getTokenNames();
254         if (checkTokens.isEmpty()) {
255             tokens = check.getDefaultTokens();
256         }
257         else {
258             tokens = check.getRequiredTokens();
259 
260             // register configured tokens
261             final int[] acceptableTokens = check.getAcceptableTokens();
262             Arrays.sort(acceptableTokens);
263             for (String token : checkTokens) {
264                 final int tokenId = TokenUtil.getTokenId(token);
265                 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
266                     registerCheck(tokenId, check);
267                 }
268                 else {
269                     final String message = String.format(Locale.ROOT, "Token \"%s\" was "
270                             + "not found in Acceptable tokens list in check %s",
271                             token, check.getClass().getName());
272                     throw new CheckstyleException(message);
273                 }
274             }
275         }
276         for (int element : tokens) {
277             registerCheck(element, check);
278         }
279         if (check.isCommentNodesRequired()) {
280             commentChecks.add(check);
281         }
282         else {
283             ordinaryChecks.add(check);
284         }
285     }
286 
287     /**
288      * Register a check for a specified token id.
289      *
290      * @param tokenId the id of the token
291      * @param check the check to register
292      * @throws CheckstyleException if Check is misconfigured
293      */
294     private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
295         if (check.isCommentNodesRequired()) {
296             tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
297                     .add(check);
298         }
299         else if (TokenUtil.isCommentType(tokenId)) {
300             final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
301                     + "token ('%s') and should override 'isCommentNodesRequired()' "
302                     + "method to return 'true'", check.getClass().getName(),
303                     TokenUtil.getTokenName(tokenId));
304             throw new CheckstyleException(message);
305         }
306         else {
307             tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
308                     .add(check);
309         }
310     }
311 
312     /**
313      * Initiates the walk of an AST.
314      *
315      * @param ast the root AST
316      * @param contents the contents of the file the AST was generated from.
317      * @param astState state of AST.
318      */
319     private void walk(DetailAST ast, FileContents contents,
320             AstState astState) {
321         notifyBegin(ast, contents, astState);
322         processIter(ast, astState);
323         notifyEnd(ast, astState);
324     }
325 
326     /**
327      * Notify checks that we are about to begin walking a tree.
328      *
329      * @param rootAST the root of the tree.
330      * @param contents the contents of the file the AST was generated from.
331      * @param astState state of AST.
332      */
333     private void notifyBegin(DetailAST rootAST, FileContents contents,
334             AstState astState) {
335         final Set<AbstractCheck> checks;
336 
337         if (astState == AstState.WITH_COMMENTS) {
338             checks = commentChecks;
339         }
340         else {
341             checks = ordinaryChecks;
342         }
343 
344         for (AbstractCheck check : checks) {
345             check.setFileContents(contents);
346             check.clearViolations();
347             check.beginTree(rootAST);
348         }
349     }
350 
351     /**
352      * Notify checks that we have finished walking a tree.
353      *
354      * @param rootAST the root of the tree.
355      * @param astState state of AST.
356      */
357     private void notifyEnd(DetailAST rootAST, AstState astState) {
358         final Set<AbstractCheck> checks;
359 
360         if (astState == AstState.WITH_COMMENTS) {
361             checks = commentChecks;
362         }
363         else {
364             checks = ordinaryChecks;
365         }
366 
367         for (AbstractCheck check : checks) {
368             check.finishTree(rootAST);
369             violations.addAll(check.getViolations());
370         }
371     }
372 
373     /**
374      * Notify checks that visiting a node.
375      *
376      * @param ast the node to notify for.
377      * @param astState state of AST.
378      */
379     private void notifyVisit(DetailAST ast, AstState astState) {
380         final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
381 
382         if (visitors != null) {
383             for (AbstractCheck check : visitors) {
384                 check.visitToken(ast);
385             }
386         }
387     }
388 
389     /**
390      * Notify checks that leaving a node.
391      *
392      * @param ast
393      *        the node to notify for
394      * @param astState state of AST.
395      */
396     private void notifyLeave(DetailAST ast, AstState astState) {
397         final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
398 
399         if (visitors != null) {
400             for (AbstractCheck check : visitors) {
401                 check.leaveToken(ast);
402             }
403         }
404     }
405 
406     /**
407      * Method returns list of checks.
408      *
409      * @param ast
410      *            the node to notify for
411      * @param astState
412      *            state of AST.
413      * @return list of visitors
414      */
415     private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
416         final Collection<AbstractCheck> visitors;
417         final int tokenId = ast.getType();
418 
419         if (astState == AstState.WITH_COMMENTS) {
420             visitors = tokenToCommentChecks.get(tokenId);
421         }
422         else {
423             visitors = tokenToOrdinaryChecks.get(tokenId);
424         }
425         return visitors;
426     }
427 
428     @Override
429     public void destroy() {
430         ordinaryChecks.forEach(AbstractCheck::destroy);
431         commentChecks.forEach(AbstractCheck::destroy);
432         super.destroy();
433     }
434 
435     @Override
436     public Set<String> getExternalResourceLocations() {
437         return Stream.concat(filters.stream(),
438                 Stream.concat(ordinaryChecks.stream(), commentChecks.stream()))
439             .filter(ExternalResourceHolder.class::isInstance)
440             .flatMap(resource -> {
441                 return ((ExternalResourceHolder) resource)
442                         .getExternalResourceLocations().stream();
443             })
444             .collect(Collectors.toUnmodifiableSet());
445     }
446 
447     /**
448      * Processes a node calling interested checks at each node.
449      * Uses iterative algorithm.
450      *
451      * @param root the root of tree for process
452      * @param astState state of AST.
453      */
454     private void processIter(DetailAST root, AstState astState) {
455         DetailAST curNode = root;
456         while (curNode != null) {
457             notifyVisit(curNode, astState);
458             DetailAST toVisit = curNode.getFirstChild();
459             while (curNode != null && toVisit == null) {
460                 notifyLeave(curNode, astState);
461                 toVisit = curNode.getNextSibling();
462                 curNode = curNode.getParent();
463             }
464             curNode = toVisit;
465         }
466     }
467 
468     /**
469      * Creates a new {@link SortedSet} with a deterministic order based on the
470      * Check's name before the default ordering.
471      *
472      * @return The new {@link SortedSet}.
473      */
474     private static SortedSet<AbstractCheck> createNewCheckSortedSet() {
475         return new TreeSet<>(
476                 Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName())
477                         .thenComparing(AbstractCheck::getId,
478                                 Comparator.nullsLast(Comparator.naturalOrder()))
479                         .thenComparingInt(AbstractCheck::hashCode));
480     }
481 
482     /**
483      * State of AST.
484      * Indicates whether tree contains certain nodes.
485      */
486     private enum AstState {
487 
488         /**
489          * Ordinary tree.
490          */
491         ORDINARY,
492 
493         /**
494          * AST contains comment nodes.
495          */
496         WITH_COMMENTS,
497 
498     }
499 
500 }