| /* |
| * 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); |
| } |
| } |
| } |
| } |
| } |