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