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