/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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 com.google.errorprone.util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.errorprone.VisitorState;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Kinds.KindSelector;
import com.sun.tools.javac.code.Scope;
import com.sun.tools.javac.code.Scope.WriteableScope;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ClassType;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Enter;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.comp.MemberEnter;
import com.sun.tools.javac.comp.Resolve;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.util.Name;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
/** A helper class to find all identifiers in scope at a given program point. */
public final class FindIdentifiers {
/** Finds a variable declaration with the given name that is in scope at the current location. */
public static Symbol findIdent(String name, VisitorState state) {
return findIdent(name, state, KindSelector.VAR);
}
/** Finds a declaration with the given name and type that is in scope at the current location. */
public static Symbol findIdent(String name, VisitorState state, KindSelector kind) {
ClassType enclosingClass = ASTHelpers.getType(state.findEnclosing(ClassTree.class));
if (enclosingClass == null || enclosingClass.tsym == null) {
return null;
}
Env<AttrContext> env = Enter.instance(state.context).getClassEnv(enclosingClass.tsym);
MethodTree enclosingMethod = state.findEnclosing(MethodTree.class);
if (enclosingMethod != null) {
env = MemberEnter.instance(state.context).getMethodEnv((JCMethodDecl) enclosingMethod, env);
}
try {
Method method =
Resolve.class.getDeclaredMethod("findIdent", Env.class, Name.class, KindSelector.class);
method.setAccessible(true);
Symbol result =
(Symbol) method.invoke(Resolve.instance(state.context), env, state.getName(name), kind);
return result.exists() ? result : null;
} catch (ReflectiveOperationException e) {
throw new LinkageError(e.getMessage(), e);
}
}
/**
* Finds the set of all bare variable identifiers in scope at the current location. Identifiers
* are ordered by ascending distance/scope count from the current location to match shadowing
* rules. That is, if two variables with the same simple names appear in the set, the one that
* appears first in iteration order is the one you get if you use the bare name in the source
* code.
*
* <p>We do not report variables that would require a qualfied access. We also do not handle
* wildcard imports.
*/
public static LinkedHashSet<VarSymbol> findAllIdents(VisitorState state) {
ImmutableSet.Builder<VarSymbol> result = new ImmutableSet.Builder<>();
Tree prev = state.getPath().getLeaf();
for (Tree curr : state.getPath().getParentPath()) {
switch (curr.getKind()) {
case BLOCK:
for (StatementTree stmt : ((BlockTree) curr).getStatements()) {
if (stmt.equals(prev)) {
break;
}
addIfVariable(stmt, result);
}
break;
case METHOD:
for (VariableTree param : ((MethodTree) curr).getParameters()) {
result.add(ASTHelpers.getSymbol(param));
}
break;
case CATCH:
result.add(ASTHelpers.getSymbol(((CatchTree) curr).getParameter()));
break;
case CLASS:
case INTERFACE:
case ENUM:
case ANNOTATION_TYPE:
// Collect fields declared in this class. If we are in a field initializer, only
// include fields declared before this one. JLS 8.3.3 allows forward references if the
// field is referred to by qualified name, but we don't support that.
for (Tree member : ((ClassTree) curr).getMembers()) {
if (member.equals(prev)) {
break;
}
addIfVariable(member, result);
}
// Collect inherited fields.
Type classType = ASTHelpers.getType(curr);
com.sun.tools.javac.util.List<Type> superTypes = state.getTypes().closure(classType).tail;
for (Type type : superTypes) {
Scope scope = type.tsym.members();
ImmutableList.Builder<VarSymbol> varsList = new ImmutableList.Builder<VarSymbol>();
for (Symbol var : scope.getSymbols(VarSymbol.class::isInstance)) {
varsList.add((VarSymbol) var);
}
result.addAll(varsList.build().reverse());
}
break;
case FOR_LOOP:
addAllIfVariable(((ForLoopTree) curr).getInitializer(), result);
break;
case ENHANCED_FOR_LOOP:
result.add(ASTHelpers.getSymbol(((EnhancedForLoopTree) curr).getVariable()));
break;
case TRY:
TryTree tryTree = (TryTree) curr;
boolean inResources = false;
for (Tree resource : tryTree.getResources()) {
if (resource.equals(prev)) {
inResources = true;
break;
}
}
if (inResources) {
// Case 1: we're in one of the resource declarations
for (Tree resource : tryTree.getResources()) {
if (resource.equals(prev)) {
break;
}
addIfVariable(resource, result);
}
} else if (tryTree.getBlock().equals(prev)) {
// Case 2: We're in the block (not a catch or finally)
addAllIfVariable(tryTree.getResources(), result);
}
break;
case COMPILATION_UNIT:
for (ImportTree importTree : ((CompilationUnitTree) curr).getImports()) {
if (importTree.isStatic()
&& importTree.getQualifiedIdentifier().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree memberSelectTree =
(MemberSelectTree) importTree.getQualifiedIdentifier();
Scope scope =
state
.getTypes()
.membersClosure(
ASTHelpers.getType(memberSelectTree.getExpression()),
/*skipInterface*/ false);
for (Symbol var :
scope.getSymbols(
sym ->
sym instanceof VarSymbol
&& sym.getSimpleName().equals(memberSelectTree.getIdentifier()))) {
result.add((VarSymbol) var);
}
}
}
break;
default:
// other node types don't introduce variables
break;
}
prev = curr;
}
// TODO(eaftan): switch out collector for ImmutableSet.toImmutableSet()
return result
.build()
.stream()
.filter(var -> isVisible(var, state.getPath()))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Finds all variable declarations which are unused at this point in the AST (i.e. they might be
* used further on).
*/
public static ImmutableSet<VarSymbol> findUnusedIdentifiers(VisitorState state) {
ImmutableSet.Builder<VarSymbol> definedVariables = ImmutableSet.builder();
ImmutableSet.Builder<Symbol> usedSymbols = ImmutableSet.builder();
Tree prev = state.getPath().getLeaf();
for (Tree curr : state.getPath().getParentPath()) {
createFindIdentifiersScanner(usedSymbols, prev).scan(curr, null);
switch (curr.getKind()) {
case BLOCK:
// If we see a block then walk over each statement to see if it defines a variable
for (StatementTree statement : ((BlockTree) curr).getStatements()) {
if (statement.equals(prev)) {
// break if we see the tree we have just processed so that we only consider things
// declared/used before us in the tree
break;
}
addIfVariable(statement, definedVariables);
}
break;
case FOR_LOOP:
ForLoopTree forLoop = (ForLoopTree) curr;
forLoop.getInitializer().stream().forEach(t -> addIfVariable(t, definedVariables));
break;
case ENHANCED_FOR_LOOP:
EnhancedForLoopTree enhancedFor = (EnhancedForLoopTree) curr;
addIfVariable(enhancedFor.getVariable(), definedVariables);
break;
default:
break;
}
prev = curr;
}
return ImmutableSet.copyOf(Sets.difference(definedVariables.build(), usedSymbols.build()));
}
/** Find the set of all identifiers referenced within this Tree */
public static ImmutableSet<Symbol> findReferencedIdentifiers(Tree tree) {
ImmutableSet.Builder<Symbol> builder = ImmutableSet.builder();
createFindIdentifiersScanner(builder, null).scan(tree, null);
return builder.build();
}
/** Finds all the visible fields declared or inherited in the target class */
public static List<VarSymbol> findAllFields(Type classType, VisitorState state) {
// TODO(andrewrice): Switch collector to ImmutableList.toImmutableList() when released
return state
.getTypes()
.closure(classType)
.stream()
.flatMap(
type -> {
TypeSymbol tsym = type.tsym;
if (tsym == null) {
return ImmutableList.<VarSymbol>of().stream();
}
WriteableScope scope = tsym.members();
if (scope == null) {
return ImmutableList.<VarSymbol>of().stream();
}
return ImmutableList.copyOf(scope.getSymbols(VarSymbol.class::isInstance))
.reverse()
.stream()
.map(v -> (VarSymbol) v)
.filter(v -> isVisible(v, state.getPath()));
})
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* Finds all identifiers in a tree. Takes an optional stop point as its argument: the depth-first
* walk will stop if this node is encountered.
*/
private static final TreeScanner<Void, Void> createFindIdentifiersScanner(
ImmutableSet.Builder<Symbol> builder, @Nullable Tree stoppingPoint) {
return new TreeScanner<Void, Void>() {
@Override
public Void scan(Tree tree, Void unused) {
return Objects.equals(stoppingPoint, tree) ? null : super.scan(tree, unused);
}
@Override
public Void scan(Iterable<? extends Tree> iterable, Void unused) {
if (stoppingPoint != null && iterable != null) {
ImmutableList.Builder<Tree> builder = ImmutableList.builder();
for (Tree t : iterable) {
if (stoppingPoint.equals(t)) {
break;
}
builder.add(t);
}
iterable = builder.build();
}
return super.scan(iterable, unused);
}
@Override
public Void visitIdentifier(IdentifierTree identifierTree, Void unused) {
Symbol symbol = ASTHelpers.getSymbol(identifierTree);
if (symbol != null) {
builder.add(symbol);
}
return null;
}
};
}
private static boolean isVisible(VarSymbol var, final TreePath path) {
switch (var.getKind()) {
case ENUM_CONSTANT:
case FIELD:
// TODO(eaftan): Switch collector to ImmutableList.toImmutableList() when released
List<ClassSymbol> enclosingClasses =
StreamSupport.stream(path.spliterator(), false)
.filter(tree -> tree instanceof ClassTree)
.map(ClassTree.class::cast)
.map(ASTHelpers::getSymbol)
.collect(Collectors.toCollection(ArrayList::new));
if (!var.isStatic()) {
// Instance fields are not visible if we are in a static context...
if (inStaticContext(path)) {
return false;
}
// ... or if we're in a static nested class and the instance fields are declared outside
// the enclosing static nested class (JLS 8.5.1).
if (lowerThan(
path,
(curr, unused) ->
curr instanceof ClassTree && ASTHelpers.getSymbol((ClassTree) curr).isStatic(),
(curr, unused) ->
curr instanceof ClassTree
&& ASTHelpers.getSymbol((ClassTree) curr).equals(var.owner))) {
return false;
}
}
// If we're lexically enclosed by the same class that defined var, we can access private
// fields (JLS 6.6.1).
if (enclosingClasses.contains(ASTHelpers.enclosingClass(var))) {
return true;
}
PackageSymbol enclosingPackage = ((JCCompilationUnit) path.getCompilationUnit()).packge;
Set<Modifier> modifiers = var.getModifiers();
// If we're in the same package where var was defined, we can access package-private fields
// (JLS 6.6.1).
if (Objects.equals(enclosingPackage, ASTHelpers.enclosingPackage(var))) {
return !modifiers.contains(Modifier.PRIVATE);
}
// Otherwise we can only access public and protected fields (JLS 6.6.1, plus the fact
// that the only enum constants and fields usable by simple name are either defined
// in the enclosing class or a superclass).
return modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PROTECTED);
case PARAMETER:
case LOCAL_VARIABLE:
// If we are in an anonymous inner class, lambda, or local class, any local variable or
// method parameter we access that is defined outside the anonymous class/lambda must be
// final or effectively final (JLS 8.1.3).
if (lowerThan(
path,
(curr, parent) ->
curr.getKind() == Kind.LAMBDA_EXPRESSION
|| (curr.getKind() == Kind.NEW_CLASS
&& ((NewClassTree) curr).getClassBody() != null)
|| (curr.getKind() == Kind.CLASS && parent.getKind() == Kind.BLOCK),
(curr, unused) -> Objects.equals(var.owner, ASTHelpers.getSymbol(curr)))) {
if ((var.flags() & (Flags.FINAL | Flags.EFFECTIVELY_FINAL)) == 0) {
return false;
}
}
return true;
case EXCEPTION_PARAMETER:
case RESOURCE_VARIABLE:
return true;
default:
throw new IllegalArgumentException("Unexpected variable type: " + var.getKind());
}
}
/** Returns true iff the leaf node of the {@code path} occurs in a JLS 8.3.1 static context. */
private static boolean inStaticContext(TreePath path) {
Tree prev = path.getLeaf();
path = path.getParentPath();
ClassSymbol enclosingClass =
ASTHelpers.getSymbol(ASTHelpers.findEnclosingNode(path, ClassTree.class));
ClassSymbol directSuperClass = (ClassSymbol) enclosingClass.getSuperclass().tsym;
for (Tree tree : path) {
switch (tree.getKind()) {
case METHOD:
return ASTHelpers.getSymbol(tree).isStatic();
case BLOCK: // static initializer
if (((BlockTree) tree).isStatic()) {
return true;
}
break;
case VARIABLE: // variable initializer of static variable
VariableTree variableTree = (VariableTree) tree;
VarSymbol variableSym = ASTHelpers.getSymbol(variableTree);
if (variableSym.getKind() == ElementKind.FIELD) {
return Objects.equals(variableTree.getInitializer(), prev) && variableSym.isStatic();
}
break;
case METHOD_INVOCATION: // JLS 8.8.7.1 explicit constructor invocation
MethodSymbol methodSym = ASTHelpers.getSymbol((MethodInvocationTree) tree);
if (methodSym == null) {
// sometimes javac can't resolve the symbol. In this case just assume that we are
// in a static context - this is a safe approximation in our context (checking
// visibility)
return true;
}
if (methodSym.isConstructor()
&& (Objects.equals(methodSym.owner, enclosingClass)
|| Objects.equals(methodSym.owner, directSuperClass))) {
return true;
}
break;
default:
break;
}
prev = tree;
}
return false;
}
private static void addIfVariable(Tree tree, ImmutableSet.Builder<VarSymbol> setBuilder) {
if (tree.getKind() == Kind.VARIABLE) {
setBuilder.add(ASTHelpers.getSymbol((VariableTree) tree));
}
}
private static void addAllIfVariable(
List<? extends Tree> list, ImmutableSet.Builder<VarSymbol> setBuilder) {
for (Tree tree : list) {
addIfVariable(tree, setBuilder);
}
}
/**
* Walks up the given {@code path} and returns true iff the first node matching {@code predicate1}
* occurs lower in the AST than the first node node matching {@code predicate2}. Returns false if
* no node matches {@code predicate1} or if no node matches {@code predicate2}.
*
* @param predicate1 A {@link BiPredicate} that accepts the current node and its parent
* @param predicate2 A {@link BiPredicate} that accepts the current node and its parent
*/
private static boolean lowerThan(
TreePath path, BiPredicate<Tree, Tree> predicate1, BiPredicate<Tree, Tree> predicate2) {
int index1 = -1;
int index2 = -1;
int count = 0;
path = path.getParentPath();
while (path != null) {
Tree curr = path.getLeaf();
TreePath parentPath = path.getParentPath();
if (index1 < 0 && predicate1.test(curr, parentPath == null ? null : parentPath.getLeaf())) {
index1 = count;
}
if (index2 < 0 && predicate2.test(curr, parentPath == null ? null : parentPath.getLeaf())) {
index2 = count;
}
if (index1 >= 0 && index2 >= 0) {
break;
}
path = parentPath;
count++;
}
return (index1 >= 0) && (index1 < index2);
}
private FindIdentifiers() {}
}