001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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.CommonUtil; 045import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 046 047/** 048 * Responsible for walking an abstract syntax tree and notifying interested 049 * checks at each each node. 050 * 051 */ 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 = moduleFactory.createModule(name); 152 if (module instanceof AutomaticBean) { 153 final AutomaticBean bean = (AutomaticBean) module; 154 bean.contextualize(childContext); 155 bean.configure(childConf); 156 } 157 if (module instanceof AbstractCheck) { 158 final AbstractCheck check = (AbstractCheck) module; 159 check.init(); 160 registerCheck(check); 161 } 162 else if (module instanceof TreeWalkerFilter) { 163 final TreeWalkerFilter filter = (TreeWalkerFilter) module; 164 filters.add(filter); 165 } 166 else { 167 throw new CheckstyleException( 168 "TreeWalker is not allowed as a parent of " + name 169 + " Please review 'Parent Module' section for this Check in web" 170 + " documentation if Check is standard."); 171 } 172 } 173 174 @Override 175 protected void processFiltered(File file, FileText fileText) throws CheckstyleException { 176 // check if already checked and passed the file 177 if (CommonUtil.matchesFileExtension(file, getFileExtensions()) 178 && (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty())) { 179 final FileContents contents = new FileContents(fileText); 180 final DetailAST rootAST = JavaParser.parse(contents); 181 if (!ordinaryChecks.isEmpty()) { 182 walk(rootAST, contents, AstState.ORDINARY); 183 } 184 if (!commentChecks.isEmpty()) { 185 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST); 186 walk(astWithComments, contents, AstState.WITH_COMMENTS); 187 } 188 if (filters.isEmpty()) { 189 addMessages(messages); 190 } 191 else { 192 final SortedSet<LocalizedMessage> filteredMessages = 193 getFilteredMessages(file.getPath(), contents, rootAST); 194 addMessages(filteredMessages); 195 } 196 messages.clear(); 197 } 198 } 199 200 /** 201 * Returns filtered set of {@link LocalizedMessage}. 202 * @param fileName path to the file 203 * @param fileContents the contents of the file 204 * @param rootAST root AST element {@link DetailAST} of the file 205 * @return filtered set of messages 206 */ 207 private SortedSet<LocalizedMessage> getFilteredMessages( 208 String fileName, FileContents fileContents, DetailAST rootAST) { 209 final SortedSet<LocalizedMessage> result = new TreeSet<>(messages); 210 for (LocalizedMessage element : messages) { 211 final TreeWalkerAuditEvent event = 212 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST); 213 for (TreeWalkerFilter filter : filters) { 214 if (!filter.accept(event)) { 215 result.remove(element); 216 break; 217 } 218 } 219 } 220 return result; 221 } 222 223 /** 224 * Register a check for a given configuration. 225 * @param check the check to register 226 * @throws CheckstyleException if an error occurs 227 */ 228 private void registerCheck(AbstractCheck check) 229 throws CheckstyleException { 230 validateDefaultTokens(check); 231 final int[] tokens; 232 final Set<String> checkTokens = check.getTokenNames(); 233 if (checkTokens.isEmpty()) { 234 tokens = check.getDefaultTokens(); 235 } 236 else { 237 tokens = check.getRequiredTokens(); 238 239 //register configured tokens 240 final int[] acceptableTokens = check.getAcceptableTokens(); 241 Arrays.sort(acceptableTokens); 242 for (String token : checkTokens) { 243 final int tokenId = TokenUtil.getTokenId(token); 244 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 245 registerCheck(token, check); 246 } 247 else { 248 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 249 + "not found in Acceptable tokens list in check %s", 250 token, check.getClass().getName()); 251 throw new CheckstyleException(message); 252 } 253 } 254 } 255 for (int element : tokens) { 256 registerCheck(element, check); 257 } 258 if (check.isCommentNodesRequired()) { 259 commentChecks.add(check); 260 } 261 else { 262 ordinaryChecks.add(check); 263 } 264 } 265 266 /** 267 * Register a check for a specified token id. 268 * @param tokenId the id of the token 269 * @param check the check to register 270 * @throws CheckstyleException if Check is misconfigured 271 */ 272 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 273 registerCheck(TokenUtil.getTokenName(tokenId), check); 274 } 275 276 /** 277 * Register a check for a specified token name. 278 * @param token the name of the token 279 * @param check the check to register 280 * @throws CheckstyleException if Check is misconfigured 281 */ 282 private void registerCheck(String token, AbstractCheck check) throws CheckstyleException { 283 if (check.isCommentNodesRequired()) { 284 tokenToCommentChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check); 285 } 286 else if (TokenUtil.isCommentType(token)) { 287 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 288 + "token ('%s') and should override 'isCommentNodesRequired()' " 289 + "method to return 'true'", check.getClass().getName(), token); 290 throw new CheckstyleException(message); 291 } 292 else { 293 tokenToOrdinaryChecks.computeIfAbsent(token, empty -> new HashSet<>()).add(check); 294 } 295 } 296 297 /** 298 * Validates that check's required tokens are subset of default tokens. 299 * @param check to validate 300 * @throws CheckstyleException when validation of default tokens fails 301 */ 302 private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException { 303 if (check.getRequiredTokens().length != 0) { 304 final int[] defaultTokens = check.getDefaultTokens(); 305 Arrays.sort(defaultTokens); 306 for (final int token : check.getRequiredTokens()) { 307 if (Arrays.binarySearch(defaultTokens, token) < 0) { 308 final String message = String.format(Locale.ROOT, "Token \"%s\" from required " 309 + "tokens was not found in default tokens list in check %s", 310 token, check.getClass().getName()); 311 throw new CheckstyleException(message); 312 } 313 } 314 } 315 } 316 317 /** 318 * Initiates the walk of an AST. 319 * @param ast the root AST 320 * @param contents the contents of the file the AST was generated from. 321 * @param astState state of AST. 322 */ 323 private void walk(DetailAST ast, FileContents contents, 324 AstState astState) { 325 notifyBegin(ast, contents, astState); 326 327 // empty files are not flagged by javac, will yield ast == null 328 if (ast != null) { 329 processIter(ast, astState); 330 } 331 notifyEnd(ast, astState); 332 } 333 334 /** 335 * Notify checks that we are about to begin walking a tree. 336 * @param rootAST the root of the tree. 337 * @param contents the contents of the file the AST was generated from. 338 * @param astState state of AST. 339 */ 340 private void notifyBegin(DetailAST rootAST, FileContents contents, 341 AstState astState) { 342 final Set<AbstractCheck> checks; 343 344 if (astState == AstState.WITH_COMMENTS) { 345 checks = commentChecks; 346 } 347 else { 348 checks = ordinaryChecks; 349 } 350 351 for (AbstractCheck check : checks) { 352 check.setFileContents(contents); 353 check.clearMessages(); 354 check.beginTree(rootAST); 355 } 356 } 357 358 /** 359 * Notify checks that we have finished walking a tree. 360 * @param rootAST the root of the tree. 361 * @param astState state of AST. 362 */ 363 private void notifyEnd(DetailAST rootAST, AstState astState) { 364 final Set<AbstractCheck> checks; 365 366 if (astState == AstState.WITH_COMMENTS) { 367 checks = commentChecks; 368 } 369 else { 370 checks = ordinaryChecks; 371 } 372 373 for (AbstractCheck check : checks) { 374 check.finishTree(rootAST); 375 messages.addAll(check.getMessages()); 376 } 377 } 378 379 /** 380 * Notify checks that visiting a node. 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 * @param ast 397 * the node to notify for 398 * @param astState state of AST. 399 */ 400 private void notifyLeave(DetailAST ast, AstState astState) { 401 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState); 402 403 if (visitors != null) { 404 for (AbstractCheck check : visitors) { 405 check.leaveToken(ast); 406 } 407 } 408 } 409 410 /** 411 * Method returns list of checks. 412 * 413 * @param ast 414 * the node to notify for 415 * @param astState 416 * state of AST. 417 * @return list of visitors 418 */ 419 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) { 420 Collection<AbstractCheck> visitors = null; 421 final String tokenType = TokenUtil.getTokenName(ast.getType()); 422 423 if (astState == AstState.WITH_COMMENTS) { 424 if (tokenToCommentChecks.containsKey(tokenType)) { 425 visitors = tokenToCommentChecks.get(tokenType); 426 } 427 } 428 else { 429 if (tokenToOrdinaryChecks.containsKey(tokenType)) { 430 visitors = tokenToOrdinaryChecks.get(tokenType); 431 } 432 } 433 return visitors; 434 } 435 436 @Override 437 public void destroy() { 438 ordinaryChecks.forEach(AbstractCheck::destroy); 439 commentChecks.forEach(AbstractCheck::destroy); 440 super.destroy(); 441 } 442 443 @Override 444 public Set<String> getExternalResourceLocations() { 445 final Set<String> ordinaryChecksResources = 446 getExternalResourceLocationsOfChecks(ordinaryChecks); 447 final Set<String> commentChecksResources = 448 getExternalResourceLocationsOfChecks(commentChecks); 449 final Set<String> filtersResources = 450 getExternalResourceLocationsOfFilters(); 451 final int resultListSize = commentChecksResources.size() 452 + ordinaryChecksResources.size() 453 + filtersResources.size(); 454 final Set<String> resourceLocations = new HashSet<>(resultListSize); 455 resourceLocations.addAll(ordinaryChecksResources); 456 resourceLocations.addAll(commentChecksResources); 457 resourceLocations.addAll(filtersResources); 458 return resourceLocations; 459 } 460 461 /** 462 * Returns a set of external configuration resource locations which are used by the filters set. 463 * @return a set of external configuration resource locations which are used by the filters set. 464 */ 465 private Set<String> getExternalResourceLocationsOfFilters() { 466 final Set<String> externalConfigurationResources = new HashSet<>(); 467 filters.stream().filter(filter -> filter instanceof ExternalResourceHolder) 468 .forEach(filter -> { 469 final Set<String> checkExternalResources = 470 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 471 externalConfigurationResources.addAll(checkExternalResources); 472 }); 473 return externalConfigurationResources; 474 } 475 476 /** 477 * Returns a set of external configuration resource locations which are used by the checks set. 478 * @param checks a set of checks. 479 * @return a set of external configuration resource locations which are used by the checks set. 480 */ 481 private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) { 482 final Set<String> externalConfigurationResources = new HashSet<>(); 483 checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> { 484 final Set<String> checkExternalResources = 485 ((ExternalResourceHolder) check).getExternalResourceLocations(); 486 externalConfigurationResources.addAll(checkExternalResources); 487 }); 488 return externalConfigurationResources; 489 } 490 491 /** 492 * Processes a node calling interested checks at each node. 493 * Uses iterative algorithm. 494 * @param root the root of tree for process 495 * @param astState state of AST. 496 */ 497 private void processIter(DetailAST root, AstState astState) { 498 DetailAST curNode = root; 499 while (curNode != null) { 500 notifyVisit(curNode, astState); 501 DetailAST toVisit = curNode.getFirstChild(); 502 while (curNode != null && toVisit == null) { 503 notifyLeave(curNode, astState); 504 toVisit = curNode.getNextSibling(); 505 if (toVisit == null) { 506 curNode = curNode.getParent(); 507 } 508 } 509 curNode = toVisit; 510 } 511 } 512 513 /** 514 * State of AST. 515 * Indicates whether tree contains certain nodes. 516 */ 517 private enum AstState { 518 519 /** 520 * Ordinary tree. 521 */ 522 ORDINARY, 523 524 /** 525 * AST contains comment nodes. 526 */ 527 WITH_COMMENTS 528 529 } 530 531}