/*
* Copyright 2015 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.bugpatterns;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ImportTree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Name;
import javax.annotation.Nullable;
/**
* Logic for inspecting static imports used by
* {@link NonCanonicalStaticImport}, {@link NonCanonicalStaticMemberImport},
* and {@link UnnecessaryStaticImport}.
*/
final class StaticImports {
@AutoValue
public abstract static class StaticImportInfo {
/** @return the fully qualified name used to import the type (possibly non-canonical) */
abstract String importedName();
/** @return the fully-qualified canonical name of the type */
abstract String canonicalName();
/** The simple name of the imported member. */
abstract Optional<String> simpleName();
/** The field or variable symbol for a static non-type member import. */
abstract ImmutableSet<Symbol> members();
/**
* Returns true if the import is canonical, i.e. the fully qualified name used to import the
* type matches the scopes it was declared in.
*/
public boolean isCanonical() {
return canonicalName().equals(importedName());
}
/** Builds the canonical import statement for the type. */
public String importStatement() {
if (members().isEmpty()) {
return String.format("import %s;", canonicalName());
} else {
return String.format("import static %s.%s;", canonicalName(), simpleName().get());
}
}
private static StaticImportInfo create(String importedName, String canonicalName) {
return new AutoValue_StaticImports_StaticImportInfo(
importedName, canonicalName, Optional.<String>absent(), ImmutableSet.<Symbol>of());
}
private static StaticImportInfo create(
String importedName, String canonicalName, String simpleName, Iterable<Symbol> members) {
return new AutoValue_StaticImports_StaticImportInfo(
importedName, canonicalName, Optional.of(simpleName), ImmutableSet.copyOf(members));
}
}
/**
* Returns a {@link StaticImports} if the given import is a static single-type import.
* Returns {@code null} otherwise, e.g. because the import is non-static, or an on-demand
* import, or statically imports a field or method.
*/
@Nullable
public static StaticImportInfo tryCreate(ImportTree tree, VisitorState state) {
if (!tree.isStatic()) {
return null;
}
if (!(tree.getQualifiedIdentifier() instanceof JCTree.JCFieldAccess)) {
return null;
}
JCTree.JCFieldAccess access = (JCTree.JCFieldAccess) tree.getQualifiedIdentifier();
String importedName = access.toString();
Type result = state.getTypeFromString(importedName);
if (result == null) {
// If the full imported name isn't a type, it might be a field or
// method:
return tryAsStaticMember(access, state);
}
String canonicalName = state.getTypes().erasure(result).toString();
if (canonicalName == null) {
return null;
}
return StaticImportInfo.create(importedName, canonicalName);
}
/**
* Returns a {@code StaticImportInfo} for a static field or method import.
*/
private static StaticImportInfo tryAsStaticMember(
JCTree.JCFieldAccess access, VisitorState state) {
Name identifier = access.getIdentifier();
if (identifier.contentEquals("*")) {
// Java doesn't allow non-canonical types inside wildcard imports,
// so there's nothing to do here.
return null;
}
String importedTypeName = access.getExpression().toString();
Type importedType = state.getTypeFromString(importedTypeName);
if (importedType == null) {
return null;
}
Types types = state.getTypes();
Type canonicalType = types.erasure(importedType);
if (canonicalType == null) {
return null;
}
Symbol.TypeSymbol baseType;
{
Symbol sym = ASTHelpers.getSymbol(access.getExpression());
if (!(sym instanceof Symbol.TypeSymbol)) {
return null;
}
baseType = (Symbol.TypeSymbol) sym;
}
Symbol.PackageSymbol pkgSym =
((JCTree.JCCompilationUnit) state.getPath().getCompilationUnit()).packge;
ImmutableSet<Symbol> members = lookup(baseType, baseType, identifier, types, pkgSym);
if (members.isEmpty()) {
return null;
}
/* Find the most specific subtype that defines one of the members that is imported.
* TODO(gak): we should instead find the most specific subtype with a member that is _used_ */
Type canonicalOwner = null;
for (Symbol member : members) {
Type owner = types.erasure(member.owner.type);
if (canonicalOwner == null || types.isSubtype(owner, canonicalOwner)) {
canonicalOwner = owner;
}
}
if (canonicalOwner == null) {
return null;
}
return StaticImportInfo.create(
importedTypeName, canonicalOwner.toString(), identifier.toString(), members);
}
/**
* Looks for a field or method with the given {@code identifier}, in
* @code typeSym} or one of it's super-types or super-interfaces,
* and that is visible from the {@code start} symbol.
*/
// TODO(cushon): does javac really not expose this anywhere?
//
// Resolve.resolveInternal{Method,Field} almost work, but we don't want
// to filter on method signature.
private static ImmutableSet<Symbol> lookup(
Symbol.TypeSymbol typeSym,
Symbol.TypeSymbol start,
Name identifier,
Types types,
Symbol.PackageSymbol pkg) {
if (typeSym == null) {
return ImmutableSet.of();
}
ImmutableSet.Builder<Symbol> members = ImmutableSet.builder();
members.addAll(lookup(types.supertype(typeSym.type).tsym, start, identifier, types, pkg));
for (Type i : types.interfaces(typeSym.type)) {
members.addAll(lookup(i.tsym, start, identifier, types, pkg));
}
OUTER:
for (Symbol member : typeSym.members().getSymbolsByName(identifier)) {
if (!member.isStatic()) {
continue;
}
switch ((int) (member.flags() & Flags.AccessFlags)) {
case Flags.PRIVATE:
continue OUTER;
case 0:
case Flags.PROTECTED:
if (member.packge() != pkg) {
continue OUTER;
}
break;
case Flags.PUBLIC:
default:
break;
}
if (member.isMemberOf(start, types)) {
members.add(member);
}
}
return members.build();
}
}