001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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 @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}