blob: 6dc6e678b08e16ba214845c61b2da433d13534ee [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.android.tools.lint.checks;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.lint.checks.SupportAnnotationDetector.filterRelevantAnnotations;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Super;
/**
* Makes sure that methods call super when overriding methods.
*/
public class CallSuperDetector extends Detector implements Detector.JavaScanner {
private static final String CALL_SUPER_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CallSuper"; //$NON-NLS-1$
private static final String ON_DETACHED_FROM_WINDOW = "onDetachedFromWindow"; //$NON-NLS-1$
private static final String ON_VISIBILITY_CHANGED = "onVisibilityChanged"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CallSuperDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Missing call to super */
public static final Issue ISSUE = Issue.create(
"MissingSuperCall", //$NON-NLS-1$
"Missing Super Call",
"Some methods, such as `View#onDetachedFromWindow`, require that you also " +
"call the super implementation as part of your method.",
Category.CORRECTNESS,
9,
Severity.ERROR,
IMPLEMENTATION);
/** Constructs a new {@link CallSuperDetector} check */
public CallSuperDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(MethodDeclaration.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull final JavaContext context) {
return new ForwardingAstVisitor() {
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
ResolvedNode resolved = context.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
checkCallSuper(context, node, method);
}
return false;
}
};
}
private static void checkCallSuper(@NonNull JavaContext context,
@NonNull MethodDeclaration declaration,
@NonNull ResolvedMethod method) {
ResolvedMethod superMethod = getRequiredSuperMethod(method);
if (superMethod != null) {
if (!SuperCallVisitor.callsSuper(context, declaration, superMethod)) {
String methodName = method.getName();
String message = "Overriding method should call `super."
+ methodName + "`";
Location location = context.getLocation(declaration.astMethodName());
context.report(ISSUE, declaration, location, message);
}
}
}
/**
* Checks whether the given method overrides a method which requires the super method
* to be invoked, and if so, returns it (otherwise returns null)
*/
@Nullable
private static ResolvedMethod getRequiredSuperMethod(
@NonNull ResolvedMethod method) {
String name = method.getName();
if (ON_DETACHED_FROM_WINDOW.equals(name)) {
// No longer annotated on the framework method since it's
// now handled via onDetachedFromWindowInternal, but overriding
// is still dangerous if supporting older versions so flag
// this for now (should make annotation carry metadata like
// compileSdkVersion >= N).
if (!method.getContainingClass().isSubclassOf(CLASS_VIEW, false)) {
return null;
}
return method.getSuperMethod();
} else if (ON_VISIBILITY_CHANGED.equals(name)) {
// From Android Wear API; doesn't yet have an annotation
// but we want to enforce this right away until the AAR
// is updated to supply it once @CallSuper is available in
// the support library
if (!method.getContainingClass().isSubclassOf(
"android.support.wearable.watchface.WatchFaceService.Engine", false)) {
return null;
}
return method.getSuperMethod();
}
// Look up annotations metadata
ResolvedMethod directSuper = method.getSuperMethod();
ResolvedMethod superMethod = directSuper;
while (superMethod != null) {
Iterable<ResolvedAnnotation> annotations = superMethod.getAnnotations();
annotations = filterRelevantAnnotations(annotations);
for (ResolvedAnnotation annotation : annotations) {
String signature = annotation.getSignature();
if (CALL_SUPER_ANNOTATION.equals(signature)) {
return directSuper;
} else if (signature.endsWith(".OverrideMustInvoke")) {
// Handle findbugs annotation on the fly too
return directSuper;
}
}
superMethod = superMethod.getSuperMethod();
}
return null;
}
/** Visits a method and determines whether the method calls its super method */
private static class SuperCallVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final ResolvedMethod mMethod;
private boolean mCallsSuper;
public static boolean callsSuper(
@NonNull JavaContext context,
@NonNull MethodDeclaration methodDeclaration,
@NonNull ResolvedMethod method) {
SuperCallVisitor visitor = new SuperCallVisitor(context, method);
methodDeclaration.accept(visitor);
return visitor.mCallsSuper;
}
private SuperCallVisitor(@NonNull JavaContext context, @NonNull ResolvedMethod method) {
mContext = context;
mMethod = method;
}
@Override
public boolean visitSuper(Super node) {
ResolvedNode resolved = null;
if (node.getParent() instanceof MethodInvocation) {
resolved = mContext.resolve(node.getParent());
}
if (resolved == null) {
resolved = mContext.resolve(node);
}
if (mMethod.equals(resolved)) {
mCallsSuper = true;
return true;
}
return false;
}
@Override
public boolean visitNode(Node node) {
return mCallsSuper || super.visitNode(node);
}
}
}