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}