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 * @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}