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 checks at each node. 051 * 052 */ 053@FileStatefulCheck 054public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder { 055 056 /** Message to use when an exception occurs and should be printed as a violation. */ 057 public static final String PARSE_EXCEPTION_MSG = "parse.exception"; 058 059 /** Maps from token name to ordinary checks. */ 060 private final Map<Integer, Set<AbstractCheck>> tokenToOrdinaryChecks = 061 new HashMap<>(); 062 063 /** Maps from token name to comment checks. */ 064 private final Map<Integer, Set<AbstractCheck>> tokenToCommentChecks = 065 new HashMap<>(); 066 067 /** Registered ordinary checks, that don't use comment nodes. */ 068 private final Set<AbstractCheck> ordinaryChecks = createNewCheckSortedSet(); 069 070 /** Registered comment checks. */ 071 private final Set<AbstractCheck> commentChecks = createNewCheckSortedSet(); 072 073 /** The ast filters. */ 074 private final Set<TreeWalkerFilter> filters = new HashSet<>(); 075 076 /** The sorted set of violations. */ 077 private final SortedSet<Violation> violations = new TreeSet<>(); 078 079 /** Context of child components. */ 080 private Context childContext; 081 082 /** A factory for creating submodules (i.e. the Checks) */ 083 private ModuleFactory moduleFactory; 084 085 /** Control whether to skip files with Java parsing exceptions. */ 086 private boolean skipFileOnJavaParseException; 087 088 /** Specify severity Level to log Java parsing exceptions when they are skipped. */ 089 private SeverityLevel javaParseExceptionSeverity = SeverityLevel.ERROR; 090 091 /** 092 * Creates a new {@code TreeWalker} instance. 093 */ 094 public TreeWalker() { 095 setFileExtensions("java"); 096 } 097 098 /** 099 * Sets the module factory for creating child modules (Checks). 100 * 101 * @param moduleFactory the factory 102 */ 103 public void setModuleFactory(ModuleFactory moduleFactory) { 104 this.moduleFactory = moduleFactory; 105 } 106 107 /** 108 * Setter to control whether to skip files with Java parsing exceptions. 109 * 110 * @param skipFileOnJavaParseException whether to skip files with Java parsing errors. 111 * @since 10.18.0 112 */ 113 public void setSkipFileOnJavaParseException(boolean skipFileOnJavaParseException) { 114 this.skipFileOnJavaParseException = skipFileOnJavaParseException; 115 } 116 117 /** 118 * Setter to specify the severity level to log Java parsing exceptions when they are skipped. 119 * 120 * @param javaParseExceptionSeverity severity level to log parsing exceptions 121 * when they are skipped. 122 * @since 10.18.0 123 */ 124 public void setJavaParseExceptionSeverity(SeverityLevel javaParseExceptionSeverity) { 125 this.javaParseExceptionSeverity = javaParseExceptionSeverity; 126 } 127 128 @Override 129 public void finishLocalSetup() { 130 final DefaultContext checkContext = new DefaultContext(); 131 checkContext.add("severity", getSeverity()); 132 checkContext.add("tabWidth", String.valueOf(getTabWidth())); 133 childContext = checkContext; 134 } 135 136 /** 137 * {@inheritDoc} Creates child module. 138 */ 139 @Override 140 public void setupChild(Configuration childConf) 141 throws CheckstyleException { 142 final String name = childConf.getName(); 143 final Object module; 144 145 try { 146 module = moduleFactory.createModule(name); 147 if (module instanceof final AbstractAutomaticBean bean) { 148 bean.contextualize(childContext); 149 bean.configure(childConf); 150 } 151 } 152 catch (final CheckstyleException exc) { 153 throw new CheckstyleException("cannot initialize module " + name, exc); 154 } 155 switch (module) { 156 case AbstractCheck check -> { 157 check.init(); 158 registerCheck(check); 159 } 160 case TreeWalkerFilter filter -> filters.add(filter); 161 case null, default -> throw new CheckstyleException( 162 "TreeWalker is not allowed as a parent of " + name 163 + " Please review 'Parent Module' section for this Check in web" 164 + " documentation if Check is standard."); 165 } 166 } 167 168 /** 169 * {@inheritDoc} Processes the file. 170 * 171 * @noinspection ProhibitedExceptionThrown 172 * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey 173 * skipFileOnJavaParseException field 174 */ 175 @Override 176 protected void processFiltered(File file, FileText fileText) throws CheckstyleException { 177 // check if already checked and passed the file 178 if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) { 179 final FileContents contents = getFileContents(); 180 DetailAST rootAST = null; 181 // whether skip the procedure after parsing Java files. 182 boolean skip = false; 183 try { 184 rootAST = JavaParser.parse(contents); 185 } 186 // -@cs[IllegalCatch] There is no other way to obey skipFileOnJavaParseException field 187 catch (Exception exc) { 188 if (!skipFileOnJavaParseException) { 189 throw exc; 190 } 191 skip = true; 192 violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG, 193 new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null, 194 getClass(), null)); 195 addViolations(violations); 196 } 197 198 if (!skip) { 199 if (!ordinaryChecks.isEmpty()) { 200 walk(rootAST, contents, AstState.ORDINARY); 201 } 202 if (!commentChecks.isEmpty()) { 203 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST); 204 walk(astWithComments, contents, AstState.WITH_COMMENTS); 205 } 206 if (filters.isEmpty()) { 207 addViolations(violations); 208 } 209 else { 210 final SortedSet<Violation> filteredViolations = 211 getFilteredViolations(file.getAbsolutePath(), contents, rootAST); 212 addViolations(filteredViolations); 213 } 214 } 215 violations.clear(); 216 } 217 } 218 219 /** 220 * Returns filtered set of {@link Violation}. 221 * 222 * @param fileName path to the file 223 * @param fileContents the contents of the file 224 * @param rootAST root AST element {@link DetailAST} of the file 225 * @return filtered set of violations 226 */ 227 private SortedSet<Violation> getFilteredViolations( 228 String fileName, FileContents fileContents, DetailAST rootAST) { 229 final SortedSet<Violation> result = new TreeSet<>(violations); 230 for (Violation element : violations) { 231 final TreeWalkerAuditEvent event = 232 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST); 233 for (TreeWalkerFilter filter : filters) { 234 if (!filter.accept(event)) { 235 result.remove(element); 236 break; 237 } 238 } 239 } 240 return result; 241 } 242 243 /** 244 * Register a check for a given configuration. 245 * 246 * @param check the check to register 247 * @throws CheckstyleException if an error occurs 248 */ 249 private void registerCheck(AbstractCheck check) throws CheckstyleException { 250 final int[] tokens; 251 final Set<String> checkTokens = check.getTokenNames(); 252 if (checkTokens.isEmpty()) { 253 tokens = check.getDefaultTokens(); 254 } 255 else { 256 tokens = check.getRequiredTokens(); 257 258 // register configured tokens 259 final int[] acceptableTokens = check.getAcceptableTokens(); 260 Arrays.sort(acceptableTokens); 261 for (String token : checkTokens) { 262 final int tokenId = TokenUtil.getTokenId(token); 263 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 264 registerCheck(tokenId, check); 265 } 266 else { 267 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 268 + "not found in Acceptable tokens list in check %s", 269 token, check.getClass().getName()); 270 throw new CheckstyleException(message); 271 } 272 } 273 } 274 for (int element : tokens) { 275 registerCheck(element, check); 276 } 277 if (check.isCommentNodesRequired()) { 278 commentChecks.add(check); 279 } 280 else { 281 ordinaryChecks.add(check); 282 } 283 } 284 285 /** 286 * Register a check for a specified token id. 287 * 288 * @param tokenId the id of the token 289 * @param check the check to register 290 * @throws CheckstyleException if Check is misconfigured 291 */ 292 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 293 if (check.isCommentNodesRequired()) { 294 tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet()) 295 .add(check); 296 } 297 else if (TokenUtil.isCommentType(tokenId)) { 298 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 299 + "token ('%s') and should override 'isCommentNodesRequired()' " 300 + "method to return 'true'", check.getClass().getName(), 301 TokenUtil.getTokenName(tokenId)); 302 throw new CheckstyleException(message); 303 } 304 else { 305 tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet()) 306 .add(check); 307 } 308 } 309 310 /** 311 * Initiates the walk of an AST. 312 * 313 * @param ast the root AST 314 * @param contents the contents of the file the AST was generated from. 315 * @param astState state of AST. 316 */ 317 private void walk(DetailAST ast, FileContents contents, 318 AstState astState) { 319 notifyBegin(ast, contents, astState); 320 processIter(ast, astState); 321 notifyEnd(ast, astState); 322 } 323 324 /** 325 * Notify checks that we are about to begin walking a tree. 326 * 327 * @param rootAST the root of the tree. 328 * @param contents the contents of the file the AST was generated from. 329 * @param astState state of AST. 330 */ 331 private void notifyBegin(DetailAST rootAST, FileContents contents, 332 AstState astState) { 333 final Set<AbstractCheck> checks; 334 335 if (astState == AstState.WITH_COMMENTS) { 336 checks = commentChecks; 337 } 338 else { 339 checks = ordinaryChecks; 340 } 341 342 for (AbstractCheck check : checks) { 343 check.setFileContents(contents); 344 check.clearViolations(); 345 check.beginTree(rootAST); 346 } 347 } 348 349 /** 350 * Notify checks that we have finished walking a tree. 351 * 352 * @param rootAST the root of the tree. 353 * @param astState state of AST. 354 */ 355 private void notifyEnd(DetailAST rootAST, AstState astState) { 356 final Set<AbstractCheck> checks; 357 358 if (astState == AstState.WITH_COMMENTS) { 359 checks = commentChecks; 360 } 361 else { 362 checks = ordinaryChecks; 363 } 364 365 for (AbstractCheck check : checks) { 366 check.finishTree(rootAST); 367 violations.addAll(check.getViolations()); 368 } 369 } 370 371 /** 372 * Notify checks that visiting a node. 373 * 374 * @param ast the node to notify for. 375 * @param astState state of AST. 376 */ 377 private void notifyVisit(DetailAST ast, AstState astState) { 378 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 379 380 if (visitors != null) { 381 for (AbstractCheck check : visitors) { 382 check.visitToken(ast); 383 } 384 } 385 } 386 387 /** 388 * Notify checks that leaving a node. 389 * 390 * @param ast 391 * the node to notify for 392 * @param astState state of AST. 393 */ 394 private void notifyLeave(DetailAST ast, AstState astState) { 395 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 396 397 if (visitors != null) { 398 for (AbstractCheck check : visitors) { 399 check.leaveToken(ast); 400 } 401 } 402 } 403 404 /** 405 * Method returns list of checks. 406 * 407 * @param ast 408 * the node to notify for 409 * @param astState 410 * state of AST. 411 * @return list of visitors 412 */ 413 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) { 414 final Collection<AbstractCheck> visitors; 415 final int tokenId = ast.getType(); 416 417 if (astState == AstState.WITH_COMMENTS) { 418 visitors = tokenToCommentChecks.get(tokenId); 419 } 420 else { 421 visitors = tokenToOrdinaryChecks.get(tokenId); 422 } 423 return visitors; 424 } 425 426 @Override 427 public void destroy() { 428 ordinaryChecks.forEach(AbstractCheck::destroy); 429 commentChecks.forEach(AbstractCheck::destroy); 430 super.destroy(); 431 } 432 433 @Override 434 public Set<String> getExternalResourceLocations() { 435 return Stream.concat(filters.stream(), 436 Stream.concat(ordinaryChecks.stream(), commentChecks.stream())) 437 .filter(ExternalResourceHolder.class::isInstance) 438 .flatMap(resource -> { 439 return ((ExternalResourceHolder) resource) 440 .getExternalResourceLocations().stream(); 441 }) 442 .collect(Collectors.toUnmodifiableSet()); 443 } 444 445 /** 446 * Processes a node calling interested checks at each node. 447 * Uses iterative algorithm. 448 * 449 * @param root the root of tree for process 450 * @param astState state of AST. 451 */ 452 private void processIter(DetailAST root, AstState astState) { 453 DetailAST curNode = root; 454 while (curNode != null) { 455 notifyVisit(curNode, astState); 456 DetailAST toVisit = curNode.getFirstChild(); 457 while (curNode != null && toVisit == null) { 458 notifyLeave(curNode, astState); 459 toVisit = curNode.getNextSibling(); 460 curNode = curNode.getParent(); 461 } 462 curNode = toVisit; 463 } 464 } 465 466 /** 467 * Creates a new {@link SortedSet} with a deterministic order based on the 468 * Check's name before the default ordering. 469 * 470 * @return The new {@code SortedSet}. 471 */ 472 private static SortedSet<AbstractCheck> createNewCheckSortedSet() { 473 return new TreeSet<>( 474 Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName()) 475 .thenComparing(AbstractCheck::getId, 476 Comparator.nullsLast(Comparator.naturalOrder())) 477 .thenComparingInt(AbstractCheck::hashCode)); 478 } 479 480 /** 481 * State of AST. 482 * Indicates whether tree contains certain nodes. 483 */ 484 private enum AstState { 485 486 /** 487 * Ordinary tree. 488 */ 489 ORDINARY, 490 491 /** 492 * AST contains comment nodes. 493 */ 494 WITH_COMMENTS, 495 496 } 497 498}