blob: 8c73b2107018b29ec9e4628fd6e115af779cd7d4 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle;
import java.io.File;
import java.io.Reader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import antlr.CommonHiddenStreamToken;
import antlr.RecognitionException;
import antlr.Token;
import antlr.TokenStreamException;
import antlr.TokenStreamHiddenTokenFilter;
import antlr.TokenStreamRecognitionException;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.Context;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer;
import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
/**
* Responsible for walking an abstract syntax tree and notifying interested
* checks at each each node.
*
* @author Oliver Burn
*/
// -@cs[ClassFanOutComplexity] To resolve issue 4714, new classes were imported. Number of
// classes current class relies on currently is 27, which is above threshold 25.
// see https://github.com/checkstyle/checkstyle/issues/4714.
public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
/** Default distance between tab stops. */
private static final int DEFAULT_TAB_WIDTH = 8;
/** Maps from token name to ordinary checks. */
private final Multimap<String, AbstractCheck> tokenToOrdinaryChecks =
HashMultimap.create();
/** Maps from token name to comment checks. */
private final Multimap<String, AbstractCheck> tokenToCommentChecks =
HashMultimap.create();
/** Registered ordinary checks, that don't use comment nodes. */
private final Set<AbstractCheck> ordinaryChecks = new HashSet<>();
/** Registered comment checks. */
private final Set<AbstractCheck> commentChecks = new HashSet<>();
/** The ast filters. */
private final Set<TreeWalkerFilter> filters = new HashSet<>();
/** The sorted set of messages. */
private final SortedSet<LocalizedMessage> messages = new TreeSet<>();
/** The distance between tab stops. */
private int tabWidth = DEFAULT_TAB_WIDTH;
/** Class loader to resolve classes with. **/
private ClassLoader classLoader;
/** Context of child components. */
private Context childContext;
/** A factory for creating submodules (i.e. the Checks) */
private ModuleFactory moduleFactory;
/**
* Creates a new {@code TreeWalker} instance.
*/
public TreeWalker() {
setFileExtensions("java");
}
/**
* Sets tab width.
* @param tabWidth the distance between tab stops
*/
public void setTabWidth(int tabWidth) {
this.tabWidth = tabWidth;
}
/**
* Sets cache file.
* @deprecated Use {@link Checker#setCacheFile} instead. It does not do anything now. We just
* keep the setter for transition period to the same option in Checker. The
* method will be completely removed in Checkstyle 8.0. See
* <a href="https://github.com/checkstyle/checkstyle/issues/2883">issue#2883</a>
* @param fileName the cache file
*/
@Deprecated
public void setCacheFile(String fileName) {
// Deprecated
}
/**
* Sets classLoader to load class.
* @param classLoader class loader to resolve classes with.
*/
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Sets the module factory for creating child modules (Checks).
* @param moduleFactory the factory
*/
public void setModuleFactory(ModuleFactory moduleFactory) {
this.moduleFactory = moduleFactory;
}
@Override
public void finishLocalSetup() {
final DefaultContext checkContext = new DefaultContext();
checkContext.add("classLoader", classLoader);
checkContext.add("severity", getSeverity());
checkContext.add("tabWidth", String.valueOf(tabWidth));
childContext = checkContext;
}
/**
* {@inheritDoc} Creates child module.
* @noinspection ChainOfInstanceofChecks
*/
@Override
public void setupChild(Configuration childConf)
throws CheckstyleException {
final String name = childConf.getName();
final Object module = moduleFactory.createModule(name);
if (module instanceof AutomaticBean) {
final AutomaticBean bean = (AutomaticBean) module;
bean.contextualize(childContext);
bean.configure(childConf);
}
if (module instanceof AbstractCheck) {
final AbstractCheck check = (AbstractCheck) module;
check.init();
registerCheck(check);
}
else if (module instanceof TreeWalkerFilter) {
final TreeWalkerFilter filter = (TreeWalkerFilter) module;
filters.add(filter);
}
else {
throw new CheckstyleException(
"TreeWalker is not allowed as a parent of " + name
+ " Please review 'Parent Module' section for this Check in web"
+ " documentation if Check is standard.");
}
}
@Override
protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
// check if already checked and passed the file
if (CommonUtils.matchesFileExtension(file, getFileExtensions())) {
final String msg = "%s occurred during the analysis of file %s.";
final String fileName = file.getPath();
try {
if (!ordinaryChecks.isEmpty()
|| !commentChecks.isEmpty()) {
final FileContents contents = new FileContents(fileText);
final DetailAST rootAST = parse(contents);
if (!ordinaryChecks.isEmpty()) {
walk(rootAST, contents, AstState.ORDINARY);
}
if (!commentChecks.isEmpty()) {
final DetailAST astWithComments = appendHiddenCommentNodes(rootAST);
walk(astWithComments, contents, AstState.WITH_COMMENTS);
}
if (filters.isEmpty()) {
addMessages(messages);
}
else {
final SortedSet<LocalizedMessage> filteredMessages =
getFilteredMessages(fileName, contents, rootAST);
addMessages(filteredMessages);
}
messages.clear();
}
}
catch (final TokenStreamRecognitionException tre) {
final String exceptionMsg = String.format(Locale.ROOT, msg,
"TokenStreamRecognitionException", fileName);
throw new CheckstyleException(exceptionMsg, tre);
}
catch (RecognitionException | TokenStreamException ex) {
final String exceptionMsg = String.format(Locale.ROOT, msg,
ex.getClass().getSimpleName(), fileName);
throw new CheckstyleException(exceptionMsg, ex);
}
}
}
/**
* Returns filtered set of {@link LocalizedMessage}.
* @param fileName path to the file
* @param fileContents the contents of the file
* @param rootAST root AST element {@link DetailAST} of the file
* @return filtered set of messages
*/
private SortedSet<LocalizedMessage> getFilteredMessages(
String fileName, FileContents fileContents, DetailAST rootAST) {
final SortedSet<LocalizedMessage> result = new TreeSet<>(messages);
for (LocalizedMessage element : messages) {
final TreeWalkerAuditEvent event =
new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
for (TreeWalkerFilter filter : filters) {
if (!filter.accept(event)) {
result.remove(element);
break;
}
}
}
return result;
}
/**
* Register a check for a given configuration.
* @param check the check to register
* @throws CheckstyleException if an error occurs
*/
private void registerCheck(AbstractCheck check)
throws CheckstyleException {
validateDefaultTokens(check);
final int[] tokens;
final Set<String> checkTokens = check.getTokenNames();
if (checkTokens.isEmpty()) {
tokens = check.getDefaultTokens();
}
else {
tokens = check.getRequiredTokens();
//register configured tokens
final int[] acceptableTokens = check.getAcceptableTokens();
Arrays.sort(acceptableTokens);
for (String token : checkTokens) {
final int tokenId = TokenUtils.getTokenId(token);
if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
registerCheck(token, check);
}
else {
final String message = String.format(Locale.ROOT, "Token \"%s\" was "
+ "not found in Acceptable tokens list in check %s",
token, check.getClass().getName());
throw new CheckstyleException(message);
}
}
}
for (int element : tokens) {
registerCheck(element, check);
}
if (check.isCommentNodesRequired()) {
commentChecks.add(check);
}
else {
ordinaryChecks.add(check);
}
}
/**
* Register a check for a specified token id.
* @param tokenId the id of the token
* @param check the check to register
* @throws CheckstyleException if Check is misconfigured
*/
private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
registerCheck(TokenUtils.getTokenName(tokenId), check);
}
/**
* Register a check for a specified token name.
* @param token the name of the token
* @param check the check to register
* @throws CheckstyleException if Check is misconfigured
*/
private void registerCheck(String token, AbstractCheck check) throws CheckstyleException {
if (check.isCommentNodesRequired()) {
tokenToCommentChecks.put(token, check);
}
else if (TokenUtils.isCommentType(token)) {
final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
+ "token ('%s') and should override 'isCommentNodesRequired()' "
+ "method to return 'true'", check.getClass().getName(), token);
throw new CheckstyleException(message);
}
else {
tokenToOrdinaryChecks.put(token, check);
}
}
/**
* Validates that check's required tokens are subset of default tokens.
* @param check to validate
* @throws CheckstyleException when validation of default tokens fails
*/
private static void validateDefaultTokens(AbstractCheck check) throws CheckstyleException {
if (check.getRequiredTokens().length != 0) {
final int[] defaultTokens = check.getDefaultTokens();
Arrays.sort(defaultTokens);
for (final int token : check.getRequiredTokens()) {
if (Arrays.binarySearch(defaultTokens, token) < 0) {
final String message = String.format(Locale.ROOT, "Token \"%s\" from required "
+ "tokens was not found in default tokens list in check %s",
token, check.getClass().getName());
throw new CheckstyleException(message);
}
}
}
}
/**
* Initiates the walk of an AST.
* @param ast the root AST
* @param contents the contents of the file the AST was generated from.
* @param astState state of AST.
*/
private void walk(DetailAST ast, FileContents contents,
AstState astState) {
notifyBegin(ast, contents, astState);
// empty files are not flagged by javac, will yield ast == null
if (ast != null) {
processIter(ast, astState);
}
notifyEnd(ast, astState);
}
/**
* Notify checks that we are about to begin walking a tree.
* @param rootAST the root of the tree.
* @param contents the contents of the file the AST was generated from.
* @param astState state of AST.
*/
private void notifyBegin(DetailAST rootAST, FileContents contents,
AstState astState) {
final Set<AbstractCheck> checks;
if (astState == AstState.WITH_COMMENTS) {
checks = commentChecks;
}
else {
checks = ordinaryChecks;
}
for (AbstractCheck check : checks) {
check.setFileContents(contents);
check.clearMessages();
check.beginTree(rootAST);
}
}
/**
* Notify checks that we have finished walking a tree.
* @param rootAST the root of the tree.
* @param astState state of AST.
*/
private void notifyEnd(DetailAST rootAST, AstState astState) {
final Set<AbstractCheck> checks;
if (astState == AstState.WITH_COMMENTS) {
checks = commentChecks;
}
else {
checks = ordinaryChecks;
}
for (AbstractCheck check : checks) {
check.finishTree(rootAST);
messages.addAll(check.getMessages());
}
}
/**
* Notify checks that visiting a node.
* @param ast the node to notify for.
* @param astState state of AST.
*/
private void notifyVisit(DetailAST ast, AstState astState) {
final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
if (visitors != null) {
for (AbstractCheck check : visitors) {
check.visitToken(ast);
}
}
}
/**
* Notify checks that leaving a node.
* @param ast
* the node to notify for
* @param astState state of AST.
*/
private void notifyLeave(DetailAST ast, AstState astState) {
final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
if (visitors != null) {
for (AbstractCheck check : visitors) {
check.leaveToken(ast);
}
}
}
/**
* Method returns list of checks.
*
* @param ast
* the node to notify for
* @param astState
* state of AST.
* @return list of visitors
*/
private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
Collection<AbstractCheck> visitors = null;
final String tokenType = TokenUtils.getTokenName(ast.getType());
if (astState == AstState.WITH_COMMENTS) {
if (tokenToCommentChecks.containsKey(tokenType)) {
visitors = tokenToCommentChecks.get(tokenType);
}
}
else {
if (tokenToOrdinaryChecks.containsKey(tokenType)) {
visitors = tokenToOrdinaryChecks.get(tokenType);
}
}
return visitors;
}
/**
* Static helper method to parses a Java source file.
*
* @param contents
* contains the contents of the file
* @return the root of the AST
* @throws TokenStreamException
* if lexing failed
* @throws RecognitionException
* if parsing failed
*/
public static DetailAST parse(FileContents contents)
throws RecognitionException, TokenStreamException {
final String fullText = contents.getText().getFullText().toString();
final Reader reader = new StringReader(fullText);
final GeneratedJavaLexer lexer = new GeneratedJavaLexer(reader);
lexer.setCommentListener(contents);
lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken");
final TokenStreamHiddenTokenFilter filter =
new TokenStreamHiddenTokenFilter(lexer);
filter.hide(TokenTypes.SINGLE_LINE_COMMENT);
filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN);
final GeneratedJavaRecognizer parser =
new GeneratedJavaRecognizer(filter);
parser.setFilename(contents.getFileName());
parser.setASTNodeClass(DetailAST.class.getName());
parser.compilationUnit();
return (DetailAST) parser.getAST();
}
/**
* Parses Java source file. Result AST contains comment nodes.
* @param contents source file content
* @return DetailAST tree
* @throws RecognitionException if parser failed
* @throws TokenStreamException if lexer failed
*/
public static DetailAST parseWithComments(FileContents contents)
throws RecognitionException, TokenStreamException {
return appendHiddenCommentNodes(parse(contents));
}
@Override
public void destroy() {
ordinaryChecks.forEach(AbstractCheck::destroy);
commentChecks.forEach(AbstractCheck::destroy);
super.destroy();
}
@Override
public Set<String> getExternalResourceLocations() {
final Set<String> ordinaryChecksResources =
getExternalResourceLocationsOfChecks(ordinaryChecks);
final Set<String> commentChecksResources =
getExternalResourceLocationsOfChecks(commentChecks);
final Set<String> filtersResources =
getExternalResourceLocationsOfFilters();
final int resultListSize = commentChecksResources.size()
+ ordinaryChecksResources.size()
+ filtersResources.size();
final Set<String> resourceLocations = new HashSet<>(resultListSize);
resourceLocations.addAll(ordinaryChecksResources);
resourceLocations.addAll(commentChecksResources);
resourceLocations.addAll(filtersResources);
return resourceLocations;
}
/**
* Returns a set of external configuration resource locations which are used by the filters set.
* @return a set of external configuration resource locations which are used by the filters set.
*/
private Set<String> getExternalResourceLocationsOfFilters() {
final Set<String> externalConfigurationResources = new HashSet<>();
filters.stream().filter(filter -> filter instanceof ExternalResourceHolder)
.forEach(filter -> {
final Set<String> checkExternalResources =
((ExternalResourceHolder) filter).getExternalResourceLocations();
externalConfigurationResources.addAll(checkExternalResources);
});
return externalConfigurationResources;
}
/**
* Returns a set of external configuration resource locations which are used by the checks set.
* @param checks a set of checks.
* @return a set of external configuration resource locations which are used by the checks set.
*/
private static Set<String> getExternalResourceLocationsOfChecks(Set<AbstractCheck> checks) {
final Set<String> externalConfigurationResources = new HashSet<>();
checks.stream().filter(check -> check instanceof ExternalResourceHolder).forEach(check -> {
final Set<String> checkExternalResources =
((ExternalResourceHolder) check).getExternalResourceLocations();
externalConfigurationResources.addAll(checkExternalResources);
});
return externalConfigurationResources;
}
/**
* Processes a node calling interested checks at each node.
* Uses iterative algorithm.
* @param root the root of tree for process
* @param astState state of AST.
*/
private void processIter(DetailAST root, AstState astState) {
DetailAST curNode = root;
while (curNode != null) {
notifyVisit(curNode, astState);
DetailAST toVisit = curNode.getFirstChild();
while (curNode != null && toVisit == null) {
notifyLeave(curNode, astState);
toVisit = curNode.getNextSibling();
if (toVisit == null) {
curNode = curNode.getParent();
}
}
curNode = toVisit;
}
}
/**
* Appends comment nodes to existing AST.
* It traverses each node in AST, looks for hidden comment tokens
* and appends found comment tokens as nodes in AST.
* @param root
* root of AST.
* @return root of AST with comment nodes.
*/
private static DetailAST appendHiddenCommentNodes(DetailAST root) {
DetailAST result = root;
DetailAST curNode = root;
DetailAST lastNode = root;
while (curNode != null) {
if (isPositionGreater(curNode, lastNode)) {
lastNode = curNode;
}
CommonHiddenStreamToken tokenBefore = curNode.getHiddenBefore();
DetailAST currentSibling = curNode;
while (tokenBefore != null) {
final DetailAST newCommentNode =
createCommentAstFromToken(tokenBefore);
currentSibling.addPreviousSibling(newCommentNode);
if (currentSibling == result) {
result = newCommentNode;
}
currentSibling = newCommentNode;
tokenBefore = tokenBefore.getHiddenBefore();
}
DetailAST toVisit = curNode.getFirstChild();
while (curNode != null && toVisit == null) {
toVisit = curNode.getNextSibling();
if (toVisit == null) {
curNode = curNode.getParent();
}
}
curNode = toVisit;
}
if (lastNode != null) {
CommonHiddenStreamToken tokenAfter = lastNode.getHiddenAfter();
DetailAST currentSibling = lastNode;
while (tokenAfter != null) {
final DetailAST newCommentNode =
createCommentAstFromToken(tokenAfter);
currentSibling.addNextSibling(newCommentNode);
currentSibling = newCommentNode;
tokenAfter = tokenAfter.getHiddenAfter();
}
}
return result;
}
/**
* Checks if position of first DetailAST is greater than position of
* second DetailAST. Position is line number and column number in source
* file.
* @param ast1
* first DetailAST node.
* @param ast2
* second DetailAST node.
* @return true if position of ast1 is greater than position of ast2.
*/
private static boolean isPositionGreater(DetailAST ast1, DetailAST ast2) {
boolean isGreater = ast1.getLineNo() > ast2.getLineNo();
if (!isGreater && ast1.getLineNo() == ast2.getLineNo()) {
isGreater = ast1.getColumnNo() > ast2.getColumnNo();
}
return isGreater;
}
/**
* Create comment AST from token. Depending on token type
* SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created.
* @param token
* Token object.
* @return DetailAST of comment node.
*/
private static DetailAST createCommentAstFromToken(Token token) {
final DetailAST commentAst;
if (token.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
commentAst = createSlCommentNode(token);
}
else {
commentAst = CommonUtils.createBlockCommentNode(token);
}
return commentAst;
}
/**
* Create single-line comment from token.
* @param token
* Token object.
* @return DetailAST with SINGLE_LINE_COMMENT type.
*/
private static DetailAST createSlCommentNode(Token token) {
final DetailAST slComment = new DetailAST();
slComment.setType(TokenTypes.SINGLE_LINE_COMMENT);
slComment.setText("//");
// column counting begins from 0
slComment.setColumnNo(token.getColumn() - 1);
slComment.setLineNo(token.getLine());
final DetailAST slCommentContent = new DetailAST();
slCommentContent.setType(TokenTypes.COMMENT_CONTENT);
// column counting begins from 0
// plus length of '//'
slCommentContent.setColumnNo(token.getColumn() - 1 + 2);
slCommentContent.setLineNo(token.getLine());
slCommentContent.setText(token.getText());
slComment.addChild(slCommentContent);
return slComment;
}
/**
* State of AST.
* Indicates whether tree contains certain nodes.
*/
private enum AstState {
/**
* Ordinary tree.
*/
ORDINARY,
/**
* AST contains comment nodes.
*/
WITH_COMMENTS
}
}