blob: bb6ff12106f9bcb6d119289b804d638702393d3a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.java.editor.base.imports;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreeScanner;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.CompilationInfo.CacheClearPolicy;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.support.CancellableTreePathScanner;
import org.netbeans.modules.java.editor.base.javadoc.JavadocImports;
/**
*
* @author lahvac
*/
public class UnusedImports {
private static final Object KEY_CACHE = new Object();
public static List<TreePathHandle> computeUnusedImports(CompilationInfo info) {
final List<TreePathHandle> result = new ArrayList<>();
for (TreePath unused : process(info, new AtomicBoolean())) {
result.add(TreePathHandle.create(unused, info));
}
return result;
}
public static Collection<TreePath> process(CompilationInfo info, AtomicBoolean cancel) {
Collection<TreePath> result = (Collection<TreePath>) info.getCachedValue(KEY_CACHE);
if (result != null) return result;
DetectorVisitor v = new DetectorVisitor(info, cancel);
CompilationUnitTree cu = info.getCompilationUnit();
v.scan(cu, null);
if (cancel.get())
return null;
List<TreePath> allUnusedImports = new ArrayList<TreePath>();
for (TreePath tree : v.getUnusedImports().values()) {
if (cancel.get()) {
return null;
}
allUnusedImports.add(tree);
}
allUnusedImports = Collections.unmodifiableList(allUnusedImports);
info.putCachedValue(KEY_CACHE, allUnusedImports, CacheClearPolicy.ON_CHANGE);
return allUnusedImports;
}
private static class DetectorVisitor extends CancellableTreePathScanner<Void, Void> {
private final CompilationInfo info;
private final Map<Element, ImportTree> element2Import = new HashMap<>();
private final Set<Element> importedBySingleImport = new HashSet<>();
private final Map<String, Collection<ImportTree>> simpleName2UnresolvableImports = new HashMap<>();
private final Set<ImportTree> unresolvablePackageImports = new HashSet<>();
private final Map<ImportTree, TreePath/*ImportTree*/> import2Highlight = new HashMap<>();
private final Map<ImportTree, Integer> usageCounts = new HashMap<>();
private DetectorVisitor(CompilationInfo info, AtomicBoolean cancel) {
super(cancel);
this.info = info;
}
private void handleJavadoc(TreePath classMember) {
if (classMember == null) {
return;
}
for (Element el : JavadocImports.computeReferencedElements(info, classMember)) {
typeUsed(el, null, false);
}
}
public Map<ImportTree, TreePath/*ImportTree*/> getUnusedImports() {
Map<ImportTree, TreePath> ret = new HashMap<>(import2Highlight);
ret.keySet().removeAll(usageCounts.keySet());
return ret;
}
@Override
public Void visitClass(ClassTree node, Void p) {
handleJavadoc(getCurrentPath());
return super.visitClass(node, p);
}
@Override
public Void visitMethod(MethodTree node, Void p) {
handleJavadoc(getCurrentPath());
return super.visitMethod(node, p);
}
@Override
public Void visitVariable(VariableTree node, Void p) {
Element e = info.getTrees().getElement(getCurrentPath());
if (e != null && e.getKind().isField()) {
handleJavadoc(getCurrentPath());
}
return super.visitVariable(node, p);
}
@Override
public Void visitCompilationUnit(CompilationUnitTree tree, Void d) {
//ignore package X.Y.Z;:
//scan(tree.getPackageDecl(), p);
scan(tree.getImports(), d);
scan(tree.getPackageAnnotations(), d);
scan(tree.getTypeDecls(), d);
return null;
}
@Override
public Void visitIdentifier(IdentifierTree tree, Void d) {
if (info.getTrees().getSourcePositions().getStartPosition(getCurrentPath().getCompilationUnit(), tree) < 0)
return null;
typeUsed(info.getTrees().getElement(getCurrentPath()), getCurrentPath(), getCurrentPath().getParentPath().getLeaf().getKind() == Kind.METHOD_INVOCATION);
return super.visitIdentifier(tree, null);
}
private boolean isStar(ImportTree tree) {
Tree qualIdent = tree.getQualifiedIdentifier();
if (qualIdent == null || qualIdent.getKind() == Kind.IDENTIFIER) {
return false;
}
return ((MemberSelectTree) qualIdent).getIdentifier().contentEquals("*");
}
private boolean parseErrorInImport(ImportTree imp) {
if (isStar(imp)) return false;
final StringBuilder fqn = new StringBuilder();
new ErrorAwareTreeScanner<Void, Void>() {
@Override
public Void visitMemberSelect(MemberSelectTree node, Void p) {
super.visitMemberSelect(node, p);
fqn.append('.');
fqn.append(node.getIdentifier());
return null;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void p) {
fqn.append(node.getName());
return null;
}
}.scan(imp.getQualifiedIdentifier(), null);
return !SourceVersion.isName(fqn);
}
public boolean isErroneous(@NullAllowed Element e) {
if (e == null) {
return true;
}
if (e.getKind() == ElementKind.MODULE) {
return false;
}
final TypeMirror type = e.asType();
if (type == null) {
return false;
}
return type.getKind() == TypeKind.ERROR || type.getKind() == TypeKind.OTHER;
}
@Override
public Void visitImport(ImportTree tree, Void d) {
if (parseErrorInImport(tree)) {
return super.visitImport(tree, null);
}
if (tree.getQualifiedIdentifier() == null ||
tree.getQualifiedIdentifier().getKind() != Tree.Kind.MEMBER_SELECT) {
return super.visitImport(tree, null);
}
MemberSelectTree qualIdent = (MemberSelectTree) tree.getQualifiedIdentifier();
boolean assign = false;
// static imports and star imports only use the qualifier part
boolean star = isStar(tree);
TreePath tp = tree.isStatic() || star ?
new TreePath(new TreePath(getCurrentPath(), qualIdent), qualIdent.getExpression()) :
new TreePath(getCurrentPath(), tree.getQualifiedIdentifier());
Element decl = info.getTrees().getElement(tp);
import2Highlight.put(tree, getCurrentPath());
if (decl != null && !isErroneous(decl)) {
if (!tree.isStatic()) {
if (star) {
List<TypeElement> types = ElementFilter.typesIn(decl.getEnclosedElements());
for (TypeElement te : types) {
assign = true;
if (!element2Import.containsKey(te)) {
element2Import.put(te, tree);
}
}
} else {
element2Import.put(decl, tree);
importedBySingleImport.add(decl);
}
} else if (decl.getKind().isClass() || decl.getKind().isInterface()) {
Name simpleName = star ? null : qualIdent.getIdentifier();
for (Element e : info.getElements().getAllMembers((TypeElement) decl)) {
if (!e.getModifiers().contains(Modifier.STATIC)) continue;
if (simpleName != null && !e.getSimpleName().equals(simpleName)) {
continue;
}
if (!star || !element2Import.containsKey(e)) {
element2Import.put(e, tree);
}
assign = true;
}
}
}
if (!assign) {
if (!tree.isStatic() && star) {
unresolvablePackageImports.add(tree);
} else {
addUnresolvableImport(qualIdent.getIdentifier(), tree);
}
}
super.visitImport(tree, null);
return null;
}
private void addUnresolvableImport(Name name, ImportTree imp) {
String key = name.toString();
Collection<ImportTree> l = simpleName2UnresolvableImports.get(key);
if (l == null) {
simpleName2UnresolvableImports.put(key, l = new LinkedList<ImportTree>());
}
l.add(imp);
}
private void addUsage(ImportTree imp) {
Integer i = usageCounts.get(imp);
if (i == null) {
i = 0;
}
usageCounts.put(imp, i++);
}
private void typeUsed(Element decl, TreePath expr, boolean methodInvocation) {
if (decl != null && (expr == null || expr.getLeaf().getKind() == Kind.IDENTIFIER || expr.getLeaf().getKind() == Kind.PARAMETERIZED_TYPE)) {
if (!isErroneous(decl)) {
ImportTree imp = element2Import.get(decl);
if (imp != null) {
addUsage(imp);
if (isStar(imp)) {
//TODO: explain
handleUnresolvableImports(decl, methodInvocation, false);
}
}
} else {
handleUnresolvableImports(decl, methodInvocation, true);
for (Entry<Element, ImportTree> e : element2Import.entrySet()) {
if (importedBySingleImport.contains(e.getKey())) continue;
if (e.getKey().getSimpleName().equals(decl.getSimpleName())) {
import2Highlight.remove(e.getValue());
}
}
}
}
}
private void handleUnresolvableImports(Element decl,
boolean methodInvocation, boolean removeStarImports) {
Name simpleName = decl.getSimpleName();
if (simpleName != null) {
Collection<ImportTree> imps = simpleName2UnresolvableImports.get(simpleName.toString());
if (imps != null) {
for (ImportTree imp : imps) {
if (!methodInvocation || imp.isStatic()) {
import2Highlight.remove(imp);
}
}
} else {
if (removeStarImports) {
//TODO: explain
for (ImportTree unresolvable : unresolvablePackageImports) {
if (!methodInvocation || unresolvable.isStatic()) {
import2Highlight.remove(unresolvable);
}
}
}
}
}
}
}
}