blob: d24a17deb8760f5c4312c6f3501dcfc2999d699f [file] [log] [blame]
/*
* Copyright (C) 2012 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.CONSTRUCTOR_NAME;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.ClassScanner;
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.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
/** Checks for accidental overrides */
public class OverrideDetector extends Detector implements ClassScanner {
/** Accidental overrides */
public static final Issue ISSUE =
Issue.create(
"DalvikOverride",
"Method considered overridden by Dalvik",
"The Dalvik virtual machine will treat a package private method in one "
+ "class as overriding a package private method in its super class, even if "
+ "they are in separate packages.\n"
+ "\n"
+ "If you really did intend for this method to override the other, make the "
+ "method `protected` instead.\n"
+ "\n"
+ "If you did **not** intend the override, consider making the method private, or "
+ "changing its name or signature.\n"
+ "\n"
+ "Note that this check is disabled be default, because ART (the successor "
+ "to Dalvik) no longer has this behavior.",
Category.CORRECTNESS,
7,
Severity.ERROR,
new Implementation(
OverrideDetector.class, EnumSet.of(Scope.ALL_CLASS_FILES)))
.setEnabledByDefault(false)
.setAndroidSpecific(true);
/** map from owner class name to JVM signatures for its package private methods */
private final Map<String, Set<String>> mPackagePrivateMethods = Maps.newHashMap();
/** Map from owner to signature to super class being overridden */
private Map<String, Map<String, String>> mErrors;
/**
* Map from owner to signature to corresponding location. When there are errors a single error
* can have locations for both the overriding and overridden methods.
*/
private Map<String, Map<String, Location>> mLocations;
/** Constructs a new {@link OverrideDetector} */
public OverrideDetector() {}
@Override
public void afterCheckRootProject(@NonNull Context context) {
// Process the check in two passes:
//
// In the first pass, gather the full set of package private methods for
// each class.
// When all classes have been processed at the end of the first pass,
// find out whether any of the methods are potentially overriding those
// in its super classes.
//
// If so, request a second pass. In the second pass, we gather full locations
// for both the base and overridden method calls, and store these.
// If the location is found to be in a suppressed context, remove that error
// entry.
//
// At the end of the second pass, we generate the errors, combining locations
// from both the overridden and overriding methods.
if (context.getPhase() == 1) {
Set<String> classes = mPackagePrivateMethods.keySet();
LintDriver driver = context.getDriver();
for (String owner : classes) {
Set<String> methods = mPackagePrivateMethods.get(owner);
String superClass = driver.getSuperClass(owner);
int packageIndex = owner.lastIndexOf('/');
while (superClass != null) {
int superPackageIndex = superClass.lastIndexOf('/');
// Only compare methods that differ in packages
if (packageIndex == -1
|| superPackageIndex != packageIndex
|| !owner.regionMatches(0, superClass, 0, packageIndex)) {
Set<String> superMethods = mPackagePrivateMethods.get(superClass);
if (superMethods != null) {
SetView<String> intersection = Sets.intersection(methods, superMethods);
if (!intersection.isEmpty()) {
if (mLocations == null) {
mLocations = Maps.newHashMap();
}
// We need a separate data structure to keep track of which
// signatures are in error,
if (mErrors == null) {
mErrors = Maps.newHashMap();
}
for (String signature : intersection) {
Map<String, Location> locations = mLocations.get(owner);
if (locations == null) {
locations = Maps.newHashMap();
mLocations.put(owner, locations);
}
locations.put(signature, null);
locations = mLocations.get(superClass);
if (locations == null) {
locations = Maps.newHashMap();
mLocations.put(superClass, locations);
}
locations.put(signature, null);
Map<String, String> errors = mErrors.get(owner);
if (errors == null) {
errors = Maps.newHashMap();
mErrors.put(owner, errors);
}
errors.put(signature, superClass);
}
}
}
}
superClass = driver.getSuperClass(superClass);
}
}
//noinspection VariableNotUsedInsideIf
if (mErrors != null) {
context.requestRepeat(this, ISSUE.getImplementation().getScope());
}
} else {
assert context.getPhase() == 2;
for (Entry<String, Map<String, String>> ownerEntry : mErrors.entrySet()) {
String owner = ownerEntry.getKey();
Map<String, String> methodToSuper = ownerEntry.getValue();
for (Entry<String, String> entry : methodToSuper.entrySet()) {
String signature = entry.getKey();
String superClass = entry.getValue();
Map<String, Location> ownerLocations = mLocations.get(owner);
if (ownerLocations != null) {
Location location = ownerLocations.get(signature);
if (location != null) {
Map<String, Location> superLocations = mLocations.get(superClass);
if (superLocations != null) {
Location superLocation = superLocations.get(signature);
if (superLocation != null) {
location.setSecondary(superLocation);
superLocation.setMessage(
"This method is treated as overridden");
}
}
String methodName = signature;
int index = methodName.indexOf('(');
if (index != -1) {
methodName = methodName.substring(0, index);
}
String message =
String.format(
"This package private method may be unintentionally "
+ "overriding `%1$s` in `%2$s`",
methodName, ClassContext.getFqcn(superClass));
context.report(ISSUE, location, message);
}
}
}
}
}
}
@SuppressWarnings("rawtypes") // ASM5 API
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
List methodList = classNode.methods;
if (context.getPhase() == 1) {
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
int access = method.access;
// Only record non-static package private methods
if ((access & (ACC_STATIC | ACC_PRIVATE | ACC_PROTECTED | ACC_PUBLIC)) != 0) {
continue;
}
// Ignore constructors too
if (CONSTRUCTOR_NAME.equals(method.name)) {
continue;
}
String owner = classNode.name;
Set<String> methods = mPackagePrivateMethods.get(owner);
if (methods == null) {
methods = Sets.newHashSetWithExpectedSize(methodList.size());
mPackagePrivateMethods.put(owner, methods);
}
methods.add(method.name + method.desc);
}
} else {
assert context.getPhase() == 2;
Map<String, Location> methods = mLocations.get(classNode.name);
if (methods == null) {
// No locations needed from this class
return;
}
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
String signature = method.name + method.desc;
if (methods.containsKey(signature)) {
if (context.getDriver().isSuppressed(ISSUE, classNode, method, null)) {
Map<String, String> errors = mErrors.get(classNode.name);
if (errors != null) {
errors.remove(signature);
}
continue;
}
Location location = context.getLocation(method, classNode);
methods.put(signature, location);
String description =
ClassContext.createSignature(classNode.name, method.name, method.desc);
location.setData(description);
}
}
}
}
}