/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.checkstyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TextBlock;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
/**
* This is a specialization of the {@link UnusedImportsCheck} that fixes a couple of problems, including correctly processing
* {@code @link} expressions that are not properly terminated on the same line, correctly processing JavaDoc {@code @param} lines,
* and correctly processing method parameters contained with {@code @link} expressions.
* <p>
* Unfortunately, the base class is not easily overwritten, and thus a fair amount of the logic has to be incorporated here.
* </p>
*
* @author Randall Hauch (rhauch@redhat.com)
*/
public class UnusedImports extends UnusedImportsCheck {
private static final String[] DEBUG_CLASSNAMES = {};// {"ModeShapeSingleUseTest"};
private static final Set<String> DEBUG_CLASSNAMES_SET = new HashSet<String>(Arrays.asList(DEBUG_CLASSNAMES));
/**
* A regular expression for finding the first word within a JavaDoc "@link" text.
*
* <pre>
* (.*?)(?:\s+|#|\$)(.*)
* </pre>
*/
private static final Pattern LINK_VALUE_IN_TEXT_PATTERN = CommonUtils.createPattern("(.*?)(?:\\s+|#|\\$)(.*)");
/**
* A regular expression for finding the class name (group 1) and the method parameters (group 2) within a JavaDoc "@link"
* reference.
*
* <pre>
* ([\w.]+)(?:\#?\w+)?(?:\(([^\)]+)\))?.*
* </pre>
*/
private static final Pattern PARTS_OF_CLASS_OR_REFERENCE_PATTERN = CommonUtils.createPattern(
"([\\w.]+)(?:\\#?\\w+)?(?:\\(([^\\)]+)\\))?.*");
/**
* A regular expression for finding the first classname referenced in a "@link" reference.
*
* <pre>
* \{\@link\s+([^}]*)
* </pre>
*/
private static final Pattern LINK_VALUE_PATTERN = CommonUtils.createPattern("\\{\\@link\\s+([^}]*)");
private boolean collect = false;
private boolean processJavaDoc = false;
private final Set<FullIdent> imports = new HashSet<>();
private final Set<String> referenced = new HashSet<>();
private boolean print = false;
public UnusedImports() {
}
@Override
public void setProcessJavadoc( boolean aValue ) {
super.setProcessJavadoc(aValue);
processJavaDoc = aValue;
}
@Override
public void beginTree( DetailAST aRootAST ) {
collect = false;
imports.clear();
referenced.clear();
super.beginTree(aRootAST);
}
@Override
public void visitToken( DetailAST aAST ) {
if (aAST.getType() == TokenTypes.CLASS_DEF) {
String classname = aAST.findFirstToken(TokenTypes.IDENT).getText();
print = DEBUG_CLASSNAMES_SET.contains(classname);
}
if (aAST.getType() == TokenTypes.IDENT) {
super.visitToken(aAST);
if (collect) processIdent(aAST);
} else if (aAST.getType() == TokenTypes.IMPORT) {
super.visitToken(aAST);
processImport(aAST);
} else if (aAST.getType() == TokenTypes.STATIC_IMPORT) {
super.visitToken(aAST);
processStaticImport(aAST);
} else {
collect = true;
if (processJavaDoc) {
processJavaDocLinkParameters(aAST);
super.visitToken(aAST);
}
}
}
/**
* Collects references made by IDENT.
*
* @param aAST the IDENT node to process {@link ArrayList stuff}
*/
protected void processIdent( DetailAST aAST ) {
final int parentType = aAST.getParent().getType();
if (((parentType != TokenTypes.DOT) && (parentType != TokenTypes.METHOD_DEF))
|| ((parentType == TokenTypes.DOT) && (aAST.getNextSibling() != null))) {
referenced.add(aAST.getText());
}
}
protected void processJavaDocLinkParameters( DetailAST aAST ) {
final FileContents contents = getFileContents();
final int lineNo = aAST.getLineNo();
final TextBlock cmt = contents.getJavadocBefore(lineNo);
if (cmt != null) {
final JavadocTags tags = JavaDocUtil.getJavadocTags(cmt, JavadocUtils.JavadocTagType.ALL);
for (final JavadocTag tag : tags.getValidTags()) {
processJavaDocTag(tag);
}
for (final InvalidJavadocTag tag : tags.getInvalidTags()) {
log(tag.getLine(), tag.getCol(), "import.invalidJavaDocTag", tag.getName());
}
}
}
protected void processJavaDocTag( JavadocTag tag ) {
print("tag: ", tag);
if (tag.canReferenceImports()) {
String identifier = tag.getFirstArg();
print("Found identifier: ", identifier);
referenced.add(identifier);
// Find the link to classes or methods ...
final Matcher matcher = LINK_VALUE_IN_TEXT_PATTERN.matcher(identifier);
while (matcher.find()) {
// Capture the link ...
identifier = matcher.group(1);
referenced.add(identifier);
print("Found new identifier: ", identifier);
// Get the parameters ...
String methodCall = matcher.group(2);
processClassOrMethodReference(methodCall);
}
} else if (tag.isParamTag()) {
String paramText = tag.getFirstArg();
print("Found parameter text: ", paramText);
// Find the links to classe
Matcher paramsMatcher = LINK_VALUE_PATTERN.matcher(paramText);
while (paramsMatcher.find()) {
// Found a link ...
String linkValue = paramsMatcher.group(1);
processClassOrMethodReference(linkValue);
}
} else if (tag.isReturnTag()) {
String returnText = tag.getFirstArg();
print("Found return text: ", returnText);
// Find the links to classe
Matcher paramsMatcher = LINK_VALUE_PATTERN.matcher(returnText);
while (paramsMatcher.find()) {
// Found a link ...
String linkValue = paramsMatcher.group(1);
processClassOrMethodReference(linkValue);
}
}
}
protected void processClassOrMethodReference( String text ) {
print("Adding referenced: ", text);
referenced.add(text);
// Look for all the identifiers within the parameters ...
Matcher paramsMatcher = PARTS_OF_CLASS_OR_REFERENCE_PATTERN.matcher(text);
while (paramsMatcher.find()) {
// This is a link; get the parameters ...
String clazz = paramsMatcher.group(1);
String params = paramsMatcher.group(2);
if (clazz != null) {
print("Found class: ", clazz);
referenced.add(clazz);
}
if (params != null) {
print("Found params: ", params);
for (String param : params.split(",")) {
if ("...".equals(param)) continue;
param = param.replace("...", "").trim();
print("Found param: ", param);
referenced.add(param);
}
}
}
}
/**
* Collects the details of imports.
*
* @param aAST node containing the import details
*/
private void processImport( DetailAST aAST ) {
final FullIdent name = FullIdent.createFullIdentBelow(aAST);
if ((name != null) && !name.getText().endsWith(".*")) {
imports.add(name);
}
}
/**
* Collects the details of static imports.
*
* @param aAST node containing the static import details
*/
private void processStaticImport( DetailAST aAST ) {
final FullIdent name = FullIdent.createFullIdent(aAST.getFirstChild().getNextSibling());
if ((name != null) && !name.getText().endsWith(".*")) {
imports.add(name);
}
}
@Override
public void finishTree( DetailAST aRootAST ) {
// loop over all the imports to see if referenced.
for (final FullIdent imp : imports) {
if (!referenced.contains(CommonUtils.baseClassName(imp.getText()))) {
print("imp.getText(): " + CommonUtils.baseClassName(imp.getText()));
print("referenced: " + referenced);
log(imp.getLineNo(), imp.getColumnNo(), "import.unused", imp.getText());
}
}
}
private void print( Object... messages ) {
if (print) {
// CHECKSTYLE IGNORE check FOR NEXT 4 LINES
for (Object msg : messages) {
System.out.print(msg);
}
System.out.println();
}
}
}