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.HashSet; 026import java.util.Locale; 027import java.util.Set; 028import java.util.SortedSet; 029import java.util.TreeSet; 030 031import com.google.common.collect.HashMultimap; 032import com.google.common.collect.Multimap; 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.CommonUtils; 045import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 046 047/** 048 * Responsible for walking an abstract syntax tree and notifying interested 049 * checks at each each node. 050 * 051 * @author Oliver Burn 052 */ 053public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder { 054 055 /** Default distance between tab stops. */ 056 private static final int DEFAULT_TAB_WIDTH = 8; 057 058 /** Maps from token name to ordinary checks. */ 059 private final Multimap<String, AbstractCheck> tokenToOrdinaryChecks = 060 HashMultimap.create(); 061 062 /** Maps from token name to comment checks. */ 063 private final Multimap<String, AbstractCheck> tokenToCommentChecks = 064 HashMultimap.create(); 065 066 /** Registered ordinary checks, that don't use comment nodes. */ 067 private final Set<AbstractCheck> ordinaryChecks = new HashSet<>(); 068 069 /** Registered comment checks. */ 070 private final Set<AbstractCheck> commentChecks = new HashSet<>(); 071 072 /** The ast filters. */ 073 private final Set<TreeWalkerFilter> filters = new HashSet<>(); 074 075 /** The sorted set of messages. */ 076 private final SortedSet<LocalizedMessage> messages = new TreeSet<>(); 077 078 /** The distance between tab stops. */ 079 private int tabWidth = DEFAULT_TAB_WIDTH; 080 081 /** Class loader to resolve classes with. **/ 082 private ClassLoader classLoader; 083 084 /** Context of child components. */ 085 private Context childContext; 086 087 /** A factory for creating submodules (i.e. the Checks) */ 088 private ModuleFactory moduleFactory; 089 090 /** 091 * Creates a new {@code TreeWalker} instance. 092 */ 093 public TreeWalker() { 094 setFileExtensions("java"); 095 } 096 097 /** 098 * Sets tab width. 099 * @param tabWidth the distance between tab stops 100 */ 101 public void setTabWidth(int tabWidth) { 102 this.tabWidth = tabWidth; 103 } 104 105 /** 106 * Sets cache file. 107 * @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just 108 * keep the setter for transition period to the same option in Checker. The 109 * method will be completely removed in Checkstyle 8.0. See 110 * <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a> 111 * @param fileName the cache file 112 */ 113 @Deprecated 114 public void setCacheFile(String fileName) { 115 // Deprecated 116 } 117 118 /** 119 * Sets classLoader to load class. 120 * @param classLoader class loader to resolve classes with. 121 */ 122 public void setClassLoader(ClassLoader classLoader) { 123 this.classLoader = classLoader; 124 } 125 126 /** 127 * Sets the module factory for creating child modules (Checks). 128 * @param moduleFactory the factory 129 */ 130 public void setModuleFactory(ModuleFactory moduleFactory) { 131 this.moduleFactory = moduleFactory; 132 } 133 134 @Override 135 public void finishLocalSetup() { 136 final DefaultContext checkContext = new DefaultContext(); 137 checkContext.add("classLoader", classLoader); 138 checkContext.add("severity", getSeverity()); 139 checkContext.add("tabWidth", String.valueOf(tabWidth)); 140 141 childContext = checkContext; 142 } 143 144 /** 145 * {@inheritDoc} Creates child module. 146 * @noinspection ChainOfInstanceofChecks 147 */ 148 @Override 149 public void setupChild(Configuration childConf) 150 throws CheckstyleException { 151 final String name = childConf.getName(); 152 final Object module = moduleFactory.createModule(name); 153 if (module instanceof AutomaticBean) { 154 final AutomaticBean bean = (AutomaticBean) module; 155 bean.contextualize(childContext); 156 bean.configure(childConf); 157 } 158 if (module instanceof AbstractCheck) { 159 final AbstractCheck check = (AbstractCheck) module; 160 check.init(); 161 registerCheck(check); 162 } 163 else if (module instanceof TreeWalkerFilter) { 164 final TreeWalkerFilter filter = (TreeWalkerFilter) module; 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 @Override 176 protected void processFiltered(File file, FileText fileText) throws CheckstyleException { 177 // check if already checked and passed the file 178 if (CommonUtils.matchesFileExtension(file, getFileExtensions()) 179 && (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty())) { 180 final FileContents contents = new FileContents(fileText); 181 final DetailAST rootAST = JavaParser.parse(contents); 182 if (!ordinaryChecks.isEmpty()) { 183 walk(rootAST, contents, AstState.ORDINARY); 184 } 185 if (!commentChecks.isEmpty()) { 186 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST); 187 walk(astWithComments, contents, AstState.WITH_COMMENTS); 188 } 189 if (filters.isEmpty()) { 190 addMessages(messages); 191 } 192 else { 193 final SortedSet<LocalizedMessage> filteredMessages = 194 getFilteredMessages(file.getPath(), contents, rootAST); 195 addMessages(filteredMessages); 196 } 197 messages.clear(); 198 } 199 } 200 201 /** 202 * Returns filtered set of {@link LocalizedMessage}. 203 * @param fileName path to the file 204 * @param fileContents the contents of the file 205 * @param rootAST root AST element {@link DetailAST} of the file 206 * @return filtered set of messages 207 */ 208 private SortedSet<LocalizedMessage> getFilteredMessages( 209 String fileName, FileContents fileContents, DetailAST rootAST) { 210 final SortedSet<LocalizedMessage> result = new TreeSet<>(messages); 211 for (LocalizedMessage element : messages) { 212 final TreeWalkerAuditEvent event = 213 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST); 214 for (TreeWalkerFilter filter : filters) { 215 if (!filter.accept(event)) { 216 result.remove(element); 217 break; 218 } 219 } 220 } 221 return result; 222 } 223 224 /** 225 * Register a check for a given configuration. 226 * @param check the check to register 227 * @throws CheckstyleException if an error occurs 228 */ 229 private void registerCheck(AbstractCheck check) 230 throws CheckstyleException { 231 validateDefaultTokens(check); 232 final int[] tokens; 233 final Set<String> checkTokens = check.getTokenNames(); 234 if (checkTokens.isEmpty()) { 235 tokens = check.getDefaultTokens(); 236 } 237 else { 238 tokens = check.getRequiredTokens(); 239 240 //register configured tokens 241 final int[] acceptableTokens = check.getAcceptableTokens(); 242 Arrays.sort(acceptableTokens); 243 for (String token : checkTokens) { 244 final int tokenId = TokenUtils.getTokenId(token); 245 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) { 246 registerCheck(token, check); 247 } 248 else { 249 final String message = String.format(Locale.ROOT, "Token \"%s\" was " 250 + "not found in Acceptable tokens list in check %s", 251 token, check.getClass().getName()); 252 throw new CheckstyleException(message); 253 } 254 } 255 } 256 for (int element : tokens) { 257 registerCheck(element, check); 258 } 259 if (check.isCommentNodesRequired()) { 260 commentChecks.add(check); 261 } 262 else { 263 ordinaryChecks.add(check); 264 } 265 } 266 267 /** 268 * Register a check for a specified token id. 269 * @param tokenId the id of the token 270 * @param check the check to register 271 * @throws CheckstyleException if Check is misconfigured 272 */ 273 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException { 274 registerCheck(TokenUtils.getTokenName(tokenId), check); 275 } 276 277 /** 278 * Register a check for a specified token name. 279 * @param token the name of the token 280 * @param check the check to register 281 * @throws CheckstyleException if Check is misconfigured 282 */ 283 private void registerCheck(String token, AbstractCheck check) throws CheckstyleException { 284 if (check.isCommentNodesRequired()) { 285 tokenToCommentChecks.put(token, check); 286 } 287 else if (TokenUtils.isCommentType(token)) { 288 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type " 289 + "token ('%s') and should override 'isCommentNodesRequired()' " 290 + "method to return 'true'", check.getClass().getName(), token); 291 throw new CheckstyleException(message); 292 } 293 else { 294 tokenToOrdinaryChecks.put(token, check); 295 } 296 } 297 298 /** 299 * Validates that check's required tokens are subset of default tokens. 300 * @param check to validate 301 * @throws CheckstyleException when validation of default tokens fails 302 */ 303 private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException { 304 if (check.getRequiredTokens().length != 0) { 305 final int[] defaultTokens = check.getDefaultTokens(); 306 Arrays.sort(defaultTokens); 307 for (final int token : check.getRequiredTokens()) { 308 if (Arrays.binarySearch(defaultTokens, token) < 0) { 309 final String message = String.format(Locale.ROOT, "Token \"%s\" from required " 310 + "tokens was not found in default tokens list in check %s", 311 token, check.getClass().getName()); 312 throw new CheckstyleException(message); 313 } 314 } 315 } 316 } 317 318 /** 319 * Initiates the walk of an AST. 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 328 // empty files are not flagged by javac, will yield ast == null 329 if (ast != null) { 330 processIter(ast, astState); 331 } 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 Collection<AbstractCheck> visitors = null; 422 final String tokenType = TokenUtils.getTokenName(ast.getType()); 423 424 if (astState == AstState.WITH_COMMENTS) { 425 if (tokenToCommentChecks.containsKey(tokenType)) { 426 visitors = tokenToCommentChecks.get(tokenType); 427 } 428 } 429 else { 430 if (tokenToOrdinaryChecks.containsKey(tokenType)) { 431 visitors = tokenToOrdinaryChecks.get(tokenType); 432 } 433 } 434 return visitors; 435 } 436 437 @Override 438 public void destroy() { 439 ordinaryChecks.forEach(AbstractCheck::destroy); 440 commentChecks.forEach(AbstractCheck::destroy); 441 super.destroy(); 442 } 443 444 @Override 445 public Set<String> getExternalResourceLocations() { 446 final Set<String> ordinaryChecksResources = 447 getExternalResourceLocationsOfChecks(ordinaryChecks); 448 final Set<String> commentChecksResources = 449 getExternalResourceLocationsOfChecks(commentChecks); 450 final Set<String> filtersResources = 451 getExternalResourceLocationsOfFilters(); 452 final int resultListSize = commentChecksResources.size() 453 + ordinaryChecksResources.size() 454 + filtersResources.size(); 455 final Set<String> resourceLocations = new HashSet<>(resultListSize); 456 resourceLocations.addAll(ordinaryChecksResources); 457 resourceLocations.addAll(commentChecksResources); 458 resourceLocations.addAll(filtersResources); 459 return resourceLocations; 460 } 461 462 /** 463 * Returns a set of external configuration resource locations which are used by the filters set. 464 * @return a set of external configuration resource locations which are used by the filters set. 465 */ 466 private Set<String> getExternalResourceLocationsOfFilters() { 467 final Set<String> externalConfigurationResources = new HashSet<>(); 468 filters.stream().filter(filter -> filter instanceof ExternalResourceHolder) 469 .forEach(filter -> { 470 final Set<String> checkExternalResources = 471 ((ExternalResourceHolder) filter).getExternalResourceLocations(); 472 externalConfigurationResources.addAll(checkExternalResources); 473 }); 474 return externalConfigurationResources; 475 } 476 477 /** 478 * Returns a set of external configuration resource locations which are used by the checks set. 479 * @param checks a set of checks. 480 * @return a set of external configuration resource locations which are used by the checks set. 481 */ 482 private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) { 483 final Set<String> externalConfigurationResources = new HashSet<>(); 484 checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> { 485 final Set<String> checkExternalResources = 486 ((ExternalResourceHolder) check).getExternalResourceLocations(); 487 externalConfigurationResources.addAll(checkExternalResources); 488 }); 489 return externalConfigurationResources; 490 } 491 492 /** 493 * Processes a node calling interested checks at each node. 494 * Uses iterative algorithm. 495 * @param root the root of tree for process 496 * @param astState state of AST. 497 */ 498 private void processIter(DetailAST root, AstState astState) { 499 DetailAST curNode = root; 500 while (curNode != null) { 501 notifyVisit(curNode, astState); 502 DetailAST toVisit = curNode.getFirstChild(); 503 while (curNode != null && toVisit == null) { 504 notifyLeave(curNode, astState); 505 toVisit = curNode.getNextSibling(); 506 if (toVisit == null) { 507 curNode = curNode.getParent(); 508 } 509 } 510 curNode = toVisit; 511 } 512 } 513 514 /** 515 * State of AST. 516 * Indicates whether tree contains certain nodes. 517 */ 518 private enum AstState { 519 520 /** 521 * Ordinary tree. 522 */ 523 ORDINARY, 524 525 /** 526 * AST contains comment nodes. 527 */ 528 WITH_COMMENTS 529 530 } 531 532}