All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.errorprone.bugpatterns.ForOverrideChecker Maven / Gradle / Ivy

There is a newer version: 2.28.0
Show newest version
/*
 * Copyright 2014 The Error Prone Authors.
 *
 * 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.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.annotation.Nullable;
import javax.lang.model.element.Modifier;

/**
 * Verifies that methods marked {@link com.google.errorprone.annotations.ForOverride} are only
 * called from the defining class.
 *
 * 

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", 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(), /* checkResult= */ true)) { return Description.NO_MATCH; } } } } List overriddenMethods = getOverriddenMethods(state, method); for (Symbol overriddenMethod : overriddenMethods) { Type declaringClass = ASTHelpers.outermostClass(overriddenMethod).asType(); if (!ASTHelpers.isSameType(declaringClass, currentClass, state)) { 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 overriddenMethods = getOverriddenMethods(state, method); if (!overriddenMethods.isEmpty()) { MethodSymbol nearestForOverrideMethod = overriddenMethods.get(0); String customMessage = "must have protected or package-private visibility"; if (nearestForOverrideMethod.equals(method)) { // The method itself is @ForOverride but is too visible customMessage = MESSAGE_BASE + customMessage; } else { // The method overrides an @ForOverride method and expands its visibility customMessage = String.format( "Method overrides @ForOverride method %s.%s, so it %s", nearestForOverrideMethod.enclClass(), nearestForOverrideMethod, customMessage); } return buildDescription(tree).setMessage(customMessage).build(); } } return Description.NO_MATCH; } /** * Returns the method that 'directly' contains the leaf element of the given path. * *

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. */ @Nullable 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 static List 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 list = new LinkedList<>(); 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.tsym != null && !currType.tsym.equals(state.getSymtab().objectType.tsym) && !ASTHelpers.isSameType(currType, Type.noType, state)) { 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 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(), /* checkResult= */ true)) { iter.remove(); } } return list; } /** Get the outermost class/interface/enum of an element, or null if none. */ private static 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 static boolean hasAnnotation(String annotation, Symbol member) { for (Attribute.Compound attribute : member.getAnnotationMirrors()) { if (annotation.equals(attribute.type.toString())) { return true; } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy