001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.HashMap; 026import java.util.HashSet; 027import java.util.Locale; 028import java.util.Map; 029import java.util.Set; 030import java.util.SortedSet; 031import java.util.TreeSet; 032 033import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 035import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 036import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 037import com.puppycrawl.tools.checkstyle.api.Configuration; 038import com.puppycrawl.tools.checkstyle.api.Context; 039import com.puppycrawl.tools.checkstyle.api.DetailAST; 040import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder; 041import com.puppycrawl.tools.checkstyle.api.FileContents; 042import com.puppycrawl.tools.checkstyle.api.FileText; 043import com.puppycrawl.tools.checkstyle.api.LocalizedMessage; 044import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 045 046/** 047 * Responsible for walking an abstract syntax tree and notifying interested 048 * checks at each each node. 049 * 050 */ 051@FileStatefulCheck 052public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder { 053 054 /** Default distance between tab stops. */ 055 private static final int DEFAULT_TAB_WIDTH = 8; 056 057 /** Maps from token name to ordinary checks. */ 058 private final Map<String, Set<AbstractCheck>> tokenToOrdinaryChecks = 059 new HashMap<>(); 060 061 /** Maps from token name to comment checks. */ 062 private final Map<String, Set<AbstractCheck>> tokenToCommentChecks = 063 new HashMap<>(); 064 065 /** Registered ordinary checks, that don't use comment nodes. */ 066 private final Set<AbstractCheck> ordinaryChecks = new HashSet<>(); 067 068 /** Registered comment checks. */ 069 private final Set<AbstractCheck> commentChecks = new HashSet<>(); 070 071 /** The ast filters. */ 072 private final Set<TreeWalkerFilter> filters = new HashSet<>(); 073 074 /** The sorted set of messages. */ 075 private final SortedSet<LocalizedMessage> messages = new TreeSet<>(); 076 077 /** The distance between tab stops. */ 078 private int tabWidth = DEFAULT_TAB_WIDTH; 079 080 /** Class loader to resolve classes with. **/ 081 private ClassLoader classLoader; 082 083 /** Context of child components. */ 084 private Context childContext; 085 086 /** A factory for creating submodules (i.e. the Checks) */ 087 private ModuleFactory moduleFactory; 088 089 /** 090 * Creates a new {@code TreeWalker} instance. 091 */ 092 public TreeWalker() { 093 setFileExtensions("java"); 094 } 095 096 /** 097 * Sets tab width. 098 * @param tabWidth the distance between tab stops 099 */ 100 public void setTabWidth(int tabWidth) { 101 this.tabWidth = tabWidth; 102 } 103 104 /** 105 * Sets cache file. 106 * @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just 107 * keep the setter for transition period to the same option in Checker. The 108 * method will be completely removed in Checkstyle 8.0. See 109 * <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a> 110 * @param fileName the cache file 111 */ 112 @Deprecated 113 public void setCacheFile(String fileName) { 114 // Deprecated 115 } 116 117 /** 118 * Sets classLoader to load class. 119 * @param classLoader class loader to resolve classes with. 120 */ 121 public void setClassLoader(ClassLoader classLoader) { 122 this.classLoader = classLoader; 123 } 124 125 /** 126 * Sets the module factory for creating child modules (Checks). 127 * @param moduleFactory the factory 128 */ 129 public void setModuleFactory(ModuleFactory moduleFactory) { 130 this.moduleFactory = moduleFactory; 131 } 132 133 @Override 134 public void finishLocalSetup() { 135 final DefaultContext checkContext = new DefaultContext(); 136 checkContext.add("classLoader", classLoader); 137 checkContext.add("severity", getSeverity()); 138 checkContext.add("tabWidth", String.valueOf(tabWidth)); 139 140 childContext = checkContext; 141 } 142 143 /** 144 * {@inheritDoc} Creates child module. 145 * @noinspection ChainOfInstanceofChecks 146 */ 147 @Override 148 public void setupChild(Configuration childConf) 149 throws CheckstyleException { 150 final String name = childConf.getName(); 151 final Object module; 152 153 try { 154 module = moduleFactory.createModule(name); 155 if (module instanceof AutomaticBean) { 156 final AutomaticBean bean = (AutomaticBean) module; 157 bean.contextualize(childContext); 158 bean.configure(childConf); 159 } 160 } 161 catch (final CheckstyleException ex) { 162 throw new CheckstyleException("cannot initialize module " + name 163 + " - " + ex.getMessage(), ex); 164 } 165 if (module instanceof AbstractCheck) { 166 final AbstractCheck check = (AbstractCheck) module; 167 check.init(); 168 registerCheck(check); 169 } 170 else if (module instanceof TreeWalkerFilter) { 171 final TreeWalkerFilter filter = (TreeWalkerFilter) module; 172 filters.add(filter); 173 } 174 else { 175 throw new CheckstyleException( 176 "TreeWalker is not allowed as a parent of " + name 177 + " Please review 'Parent Module' section for this Check in web" 178 + " documentation if Check is standard."); 179 } 180 } 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 = new FileContents(fileText); 187 final DetailAST rootAST = JavaParser.parse(contents); 188 if (!ordinaryChecks.isEmpty()) { 189 walk(rootAST, contents, AstState.ORDINARY); 190 } 191 if (!commentChecks.isEmpty()) { 192 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST); 193 walk(astWithComments, contents, AstState.WITH_COMMENTS); 194 } 195 if (filters.isEmpty()) { 196 addMessages(messages); 197 } 198 else { 199 final SortedSet<LocalizedMessage> filteredMessages = 200 getFilteredMessages(file.getAbsolutePath(), contents, rootAST); 201 addMessages(filteredMessages); 202 } 203 messages.clear(); 204 } 205 } 206 207 /** 208 * Returns filtered set of {@link LocalizedMessage}. 209 * @param fileName path to the file 210 * @param fileContents the contents of the file 211 * @param rootAST root AST element {@link DetailAST} of the file 212 * @return filtered set of messages 213 */ 214 private SortedSet<LocalizedMessage> getFilteredMessages( 215 String fileName, FileContents fileContents, DetailAST rootAST) { 216 final SortedSet<LocalizedMessage> result = new TreeSet<>(messages); 217 for (LocalizedMessage element : messages) { 218 final TreeWalkerAuditEvent event = 219 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST); 220 for (TreeWalkerFilter filter : filters) { 221 if (!filter.accept(event)) { 222 result.remove(element); 223 break; 224 } 225 } 226 } 227 return result; 228 } 229 230 /** 231 * Register a check for a given configuration. 232 * @param check the check to register 233 * @throws CheckstyleException if an error occurs 234 */ 235 private void registerCheck(AbstractCheck check) 236 throws CheckstyleException { 237 validateDefaultTokens(check); 238 final int[] tokens; 239 final Set<String> checkTokens = check.getTokenNames(); 240 if (checkTokens.isEmpty()) { 241 tokens = check.getDefaultTokens(); 242 } 243 else { 244 tokens = check.getRequiredTokens(); 245 246 //register configured tokens 247 final int[] acceptableTokens = check.getAcceptableTokens(); 248 Arrays.sort(acceptableTokens); 249 for (String token : checkTokens) { 250 final int tokenId = TokenUtil.getTokenId(token); 251 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 252 registerCheck(token, check); 253 } 254 else { 255 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 256 + "not found in Acceptable tokens list in check %s", 257 token, check.getClass().getName()); 258 throw new CheckstyleException(message); 259 } 260 } 261 } 262 for (int element : tokens) { 263 registerCheck(element, check); 264 } 265 if (check.isCommentNodesRequired()) { 266 commentChecks.add(check); 267 } 268 else { 269 ordinaryChecks.add(check); 270 } 271 } 272 273 /** 274 * Register a check for a specified token id. 275 * @param tokenId the id of the token 276 * @param check the check to register 277 * @throws CheckstyleException if Check is misconfigured 278 */ 279 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 280 registerCheck(TokenUtil.getTokenName(tokenId), check); 281 } 282 283 /** 284 * Register a check for a specified token name. 285 * @param token the name of the token 286 * @param check the check to register 287 * @throws CheckstyleException if Check is misconfigured 288 */ 289 private void registerCheck(String token, AbstractCheck check) throws CheckstyleException { 290 if (check.isCommentNodesRequired()) { 291 tokenToCommentChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check); 292 } 293 else if (TokenUtil.isCommentType(token)) { 294 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 295 + "token ('%s') and should override 'isCommentNodesRequired()' " 296 + "method to return 'true'", check.getClass().getName(), token); 297 throw new CheckstyleException(message); 298 } 299 else { 300 tokenToOrdinaryChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check); 301 } 302 } 303 304 /** 305 * Validates that check's required tokens are subset of default tokens. 306 * @param check to validate 307 * @throws CheckstyleException when validation of default tokens fails 308 */ 309 private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException { 310 final int[] defaultTokens = check.getDefaultTokens(); 311 Arrays.sort(defaultTokens); 312 for (final int token : check.getRequiredTokens()) { 313 if (Arrays.binarySearch(defaultTokens, token) < 0) { 314 final String message = String.format(Locale.ROOT, "Token \"%s\" from required " 315 + "tokens was not found in default tokens list in check %s", 316 token, check.getClass().getName()); 317 throw new CheckstyleException(message); 318 } 319 } 320 } 321 322 /** 323 * Initiates the walk of an AST. 324 * @param ast the root AST 325 * @param contents the contents of the file the AST was generated from. 326 * @param astState state of AST. 327 */ 328 private void walk(DetailAST ast, FileContents contents, 329 AstState astState) { 330 notifyBegin(ast, contents, astState); 331 processIter(ast, astState); 332 notifyEnd(ast, astState); 333 } 334 335 /** 336 * Notify checks that we are about to begin walking a tree. 337 * @param rootAST the root of the tree. 338 * @param contents the contents of the file the AST was generated from. 339 * @param astState state of AST. 340 */ 341 private void notifyBegin(DetailAST rootAST, FileContents contents, 342 AstState astState) { 343 final Set<AbstractCheck> checks; 344 345 if (astState == AstState.WITH_COMMENTS) { 346 checks = commentChecks; 347 } 348 else { 349 checks = ordinaryChecks; 350 } 351 352 for (AbstractCheck check : checks) { 353 check.setFileContents(contents); 354 check.clearMessages(); 355 check.beginTree(rootAST); 356 } 357 } 358 359 /** 360 * Notify checks that we have finished walking a tree. 361 * @param rootAST the root of the tree. 362 * @param astState state of AST. 363 */ 364 private void notifyEnd(DetailAST rootAST, AstState astState) { 365 final Set<AbstractCheck> checks; 366 367 if (astState == AstState.WITH_COMMENTS) { 368 checks = commentChecks; 369 } 370 else { 371 checks = ordinaryChecks; 372 } 373 374 for (AbstractCheck check : checks) { 375 check.finishTree(rootAST); 376 messages.addAll(check.getMessages()); 377 } 378 } 379 380 /** 381 * Notify checks that visiting a node. 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 * @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 String tokenType = TokenUtil.getTokenName(ast.getType()); 423 424 if (astState == AstState.WITH_COMMENTS) { 425 visitors = tokenToCommentChecks.get(tokenType); 426 } 427 else { 428 visitors = tokenToOrdinaryChecks.get(tokenType); 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 final Set<String> ordinaryChecksResources = 443 getExternalResourceLocationsOfChecks(ordinaryChecks); 444 final Set<String> commentChecksResources = 445 getExternalResourceLocationsOfChecks(commentChecks); 446 final Set<String> filtersResources = 447 getExternalResourceLocationsOfFilters(); 448 final int resultListSize = commentChecksResources.size() 449 + ordinaryChecksResources.size() 450 + filtersResources.size(); 451 final Set<String> resourceLocations = new HashSet<>(resultListSize); 452 resourceLocations.addAll(ordinaryChecksResources); 453 resourceLocations.addAll(commentChecksResources); 454 resourceLocations.addAll(filtersResources); 455 return resourceLocations; 456 } 457 458 /** 459 * Returns a set of external configuration resource locations which are used by the filters set. 460 * @return a set of external configuration resource locations which are used by the filters set. 461 */ 462 private Set<String> getExternalResourceLocationsOfFilters() { 463 final Set<String> externalConfigurationResources = new HashSet<>(); 464 filters.stream().filter(filter -> filter instanceof ExternalResourceHolder) 465 .forEach(filter -> { 466 final Set<String> checkExternalResources = 467 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 468 externalConfigurationResources.addAll(checkExternalResources); 469 }); 470 return externalConfigurationResources; 471 } 472 473 /** 474 * Returns a set of external configuration resource locations which are used by the checks set. 475 * @param checks a set of checks. 476 * @return a set of external configuration resource locations which are used by the checks set. 477 */ 478 private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) { 479 final Set<String> externalConfigurationResources = new HashSet<>(); 480 checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> { 481 final Set<String> checkExternalResources = 482 ((ExternalResourceHolder) check).getExternalResourceLocations(); 483 externalConfigurationResources.addAll(checkExternalResources); 484 }); 485 return externalConfigurationResources; 486 } 487 488 /** 489 * Processes a node calling interested checks at each node. 490 * Uses iterative algorithm. 491 * @param root the root of tree for process 492 * @param astState state of AST. 493 */ 494 private void processIter(DetailAST root, AstState astState) { 495 DetailAST curNode = root; 496 while (curNode != null) { 497 notifyVisit(curNode, astState); 498 DetailAST toVisit = curNode.getFirstChild(); 499 while (curNode != null && toVisit == null) { 500 notifyLeave(curNode, astState); 501 toVisit = curNode.getNextSibling(); 502 curNode = curNode.getParent(); 503 } 504 curNode = toVisit; 505 } 506 } 507 508 /** 509 * State of AST. 510 * Indicates whether tree contains certain nodes. 511 */ 512 private enum AstState { 513 514 /** 515 * Ordinary tree. 516 */ 517 ORDINARY, 518 519 /** 520 * AST contains comment nodes. 521 */ 522 WITH_COMMENTS, 523 524 } 525 526}