001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Comparator;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.Locale;
029import java.util.Map;
030import java.util.Set;
031import java.util.SortedSet;
032import java.util.TreeSet;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035
036import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
037import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
038import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
039import com.puppycrawl.tools.checkstyle.api.Configuration;
040import com.puppycrawl.tools.checkstyle.api.Context;
041import com.puppycrawl.tools.checkstyle.api.DetailAST;
042import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
043import com.puppycrawl.tools.checkstyle.api.FileContents;
044import com.puppycrawl.tools.checkstyle.api.FileText;
045import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
046import com.puppycrawl.tools.checkstyle.api.Violation;
047import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
048
049/**
050 * Responsible for walking an abstract syntax tree and notifying interested
051 * checks at each node.
052 *
053 */
054@FileStatefulCheck
055public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
056
057 /** Message to use when an exception occurs and should be printed as a violation. */
058 public static final String PARSE_EXCEPTION_MSG = "parse.exception";
059
060 /** Maps from token name to ordinary checks. */
061 private final Map<Integer, Set<AbstractCheck>> tokenToOrdinaryChecks =
062 new HashMap<>();
063
064 /** Maps from token name to comment checks. */
065 private final Map<Integer, Set<AbstractCheck>> tokenToCommentChecks =
066 new HashMap<>();
067
068 /** Registered ordinary checks, that don't use comment nodes. */
069 private final Set<AbstractCheck> ordinaryChecks = createNewCheckSortedSet();
070
071 /** Registered comment checks. */
072 private final Set<AbstractCheck> commentChecks = createNewCheckSortedSet();
073
074 /** The ast filters. */
075 private final Set<TreeWalkerFilter> filters = new HashSet<>();
076
077 /** The sorted set of violations. */
078 private final SortedSet<Violation> violations = new TreeSet<>();
079
080 /** Context of child components. */
081 private Context childContext;
082
083 /** A factory for creating submodules (i.e. the Checks) */
084 private ModuleFactory moduleFactory;
085
086 /** Control whether to skip files with Java parsing exceptions. */
087 private boolean skipFileOnJavaParseException;
088
089 /** Specify severity Level to log Java parsing exceptions when they are skipped. */
090 private SeverityLevel javaParseExceptionSeverity = SeverityLevel.ERROR;
091
092 /**
093 * Creates a new {@code TreeWalker} instance.
094 */
095 public TreeWalker() {
096 setFileExtensions("java");
097 }
098
099 /**
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}