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