/*
* Copyright 2014 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 static com.google.errorprone.BugPattern.Category.GUAVA;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.lang.model.element.Modifier;
/**
* Verifies that methods marked {@link com.google.errorprone.annotations.ForOverride} are only
* called from the defining class.
*
* <p>Specifically, all calls to the method have to occur within the context of the outermost class
* where the method is defined.
*/
@BugPattern(
name = "ForOverride",
summary =
"Method annotated @ForOverride must be protected or package-private and only invoked from "
+ "declaring class, or from an override of the method",
explanation =
"A method that overrides a @ForOverride method should not be invoked directly. Instead, it"
+ " should be invoked only from the class in which it was declared. For example, if"
+ " overriding Converter.doForward, you should invoke it through Converter.convert."
+ " For testing, factor out the code you want to run to a separate method.",
category = GUAVA,
severity = ERROR
)
public class ForOverrideChecker extends BugChecker
implements MethodInvocationTreeMatcher, MethodTreeMatcher {
private static final String FOR_OVERRIDE = "com.google.errorprone.annotations.ForOverride";
private static final String MESSAGE_BASE = "Method annotated @ForOverride ";
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
MethodSymbol method = ASTHelpers.getSymbol(tree);
if (method == null) {
return Description.NO_MATCH;
}
Type currentClass = getOutermostClass(state);
if (method.isStatic() || method.isConstructor() || currentClass == null) {
return Description.NO_MATCH;
}
// allow super.foo() calls to @ForOverride methods from overriding methods
if (isSuperCall(currentClass, tree, state)) {
MethodTree currentMethod = findDirectMethod(state.getPath());
// currentMethod might be null if we are in a field initializer
if (currentMethod != null) {
// MethodSymbol.overrides doesn't check that names match, so we need to do that first.
if (currentMethod.getName().equals(method.name)) {
MethodSymbol currentMethodSymbol = ASTHelpers.getSymbol(currentMethod);
if (currentMethodSymbol.overrides(
method, (TypeSymbol) method.owner, state.getTypes(), true)) {
return Description.NO_MATCH;
}
}
}
}
List<MethodSymbol> overriddenMethods = getOverriddenMethods(state, method);
for (Symbol overriddenMethod : overriddenMethods) {
Type declaringClass = overriddenMethod.outermostClass().asType();
if (!declaringClass.equals(currentClass)) {
String customMessage =
MESSAGE_BASE
+ "must not be invoked directly "
+ "(except by the declaring class, "
+ declaringClass
+ ")";
return buildDescription(tree).setMessage(customMessage).build();
}
}
return Description.NO_MATCH;
}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
MethodSymbol method = ASTHelpers.getSymbol(tree);
if (method.isStatic() || method.isConstructor()) {
return Description.NO_MATCH;
}
if (method.getModifiers().contains(Modifier.PUBLIC)
|| method.getModifiers().contains(Modifier.PRIVATE)) {
List<MethodSymbol> overriddenMethods = getOverriddenMethods(state, method);
if (!overriddenMethods.isEmpty()) {
String customMessage = MESSAGE_BASE + "must have protected or package-private visibility";
return buildDescription(tree).setMessage(customMessage).build();
}
}
return Description.NO_MATCH;
}
/**
* Returns the method that 'directly' contains the leaf element of the given path.
*
* <p>By 'direct', we mean that if the leaf is part of a field initializer of a class, then it is
* considered to not be part of any method.
*/
private static MethodTree findDirectMethod(TreePath path) {
while (true) {
path = path.getParentPath();
if (path != null) {
Tree leaf = path.getLeaf();
if (leaf instanceof MethodTree) {
return (MethodTree) leaf;
}
// if we find a ClassTree before a MethodTree, we must be an initializer
if (leaf instanceof ClassTree) {
return null;
}
} else {
return null;
}
}
}
/** Returns true if this method invocation is of the form {@code super.foo()} */
private static boolean isSuperCall(Type type, MethodInvocationTree tree, VisitorState state) {
if (tree.getMethodSelect().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree select = (MemberSelectTree) tree.getMethodSelect();
if (select.getExpression().getKind() == Kind.IDENTIFIER) {
IdentifierTree ident = (IdentifierTree) select.getExpression();
return ident.getName().contentEquals("super");
} else if (select.getExpression().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree subSelect = (MemberSelectTree) select.getExpression();
return subSelect.getIdentifier().contentEquals("super")
&& ASTHelpers.isSameType(ASTHelpers.getType(subSelect.getExpression()), type, state);
}
}
return false;
}
/**
* Get overridden @ForOverride methods.
*
* @param state the VisitorState
* @param method the method to find overrides for
* @return a list of methods annotated @ForOverride that the method overrides, including the
* method itself if it has the annotation
*/
private List<MethodSymbol> getOverriddenMethods(VisitorState state, MethodSymbol method) {
// Static methods cannot override, only overload.
if (method.isStatic()) {
throw new IllegalArgumentException(
"getOverriddenMethods may not be called on a static method");
}
List<MethodSymbol> list = new LinkedList<MethodSymbol>();
list.add(method);
// Iterate over supertypes of the type that owns this method, collecting a list of all method
// symbols with the same name. We intentionally exclude interface methods because interface
// methods cannot be annotated @ForOverride. @ForOverride methods must have protected or
// package-private visibility, but interface methods have implicit public visibility.
Type currType = state.getTypes().supertype(method.owner.type);
while (currType != null
&& !currType.equals(state.getSymtab().objectType)
&& !currType.equals(Type.noType)) {
Symbol sym = currType.tsym.members().findFirst(method.name);
if (sym instanceof MethodSymbol) {
list.add((MethodSymbol) sym);
}
currType = state.getTypes().supertype(currType);
}
// Remove methods that either don't have the @ForOverride annotation or don't override the
// method in question.
Iterator<MethodSymbol> iter = list.iterator();
while (iter.hasNext()) {
MethodSymbol member = iter.next();
if (!hasAnnotation(FOR_OVERRIDE, member)
// Note that MethodSymbol.overrides() ignores static-ness, but that's OK since we've
// already checked that this method is not static.
|| !method.overrides(member, (TypeSymbol) member.owner, state.getTypes(), true)) {
iter.remove();
}
}
return list;
}
/** Get the outermost class/interface/enum of an element, or null if none. */
private Type getOutermostClass(VisitorState state) {
TreePath path = state.getPath();
Type type = null;
while (path != null) {
if (path.getLeaf().getKind() == Kind.CLASS
|| path.getLeaf().getKind() == Kind.INTERFACE
|| path.getLeaf().getKind() == Kind.ENUM) {
type = ASTHelpers.getSymbol(path.getLeaf()).type;
}
path = path.getParentPath();
}
return type;
}
private boolean hasAnnotation(String annotation, Symbol member) {
for (Attribute.Compound attribute : member.getAnnotationMirrors()) {
if (annotation.equals(attribute.type.toString())) {
return true;
}
}
return false;
}
}