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