blob: eb95cc554b736dc6839c44ef98e52eaa2bc13cf9 [file] [log] [blame]
/*
* Copyright (C) 2014 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.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.Node;
/**
* Checks that subclasses of certain APIs are overriding all methods that were abstract
* in one or more earlier API levels that are still targeted by the minSdkVersion
* of this project.
*/
public class OverrideConcreteDetector extends Detector implements JavaScanner {
/** Are previously-abstract methods all overridden? */
public static final Issue ISSUE = Issue.create(
"OverrideAbstract", //$NON-NLS-1$
"Not overriding abstract methods on older platforms",
"To improve the usability of some APIs, some methods that used to be `abstract` have " +
"been made concrete by adding default implementations. This means that when compiling " +
"with new versions of the SDK, your code does not have to override these methods.\n" +
"\n" +
"However, if your code is also targeting older versions of the platform where these " +
"methods were still `abstract`, the code will crash. You must override all methods " +
"that used to be abstract in any versions targeted by your application's " +
"`minSdkVersion`.",
Category.CORRECTNESS,
6,
Severity.FATAL,
new Implementation(
OverrideConcreteDetector.class,
Scope.JAVA_FILE_SCOPE)
);
// This check is currently hardcoded for the specific case of the
// NotificationListenerService change in API 21. We should consider
// attempting to infer this information automatically from changes in
// the API current.txt file and making this detector more database driven,
// like the API detector.
private static final String NOTIFICATION_LISTENER_SERVICE_FQN
= "android.service.notification.NotificationListenerService";
public static final String STATUS_BAR_NOTIFICATION_FQN
= "android.service.notification.StatusBarNotification";
private static final String ON_NOTIFICATION_POSTED = "onNotificationPosted";
private static final String ON_NOTIFICATION_REMOVED = "onNotificationRemoved";
private static final int CONCRETE_IN = 21;
/** Constructs a new {@link OverrideConcreteDetector} */
public OverrideConcreteDetector() {
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN);
}
@Override
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass resolvedClass) {
if (node == null) {
return;
}
int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
return;
}
int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(node));
if (minSdk >= CONCRETE_IN) {
return;
}
String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED};
for (String methodName : methodNames) {
boolean found = false;
for (ResolvedMethod method : resolvedClass.getMethods(methodName, true)) {
// Make sure it's not the base method, but that it's been defined
// in a subclass, concretely
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.matches(NOTIFICATION_LISTENER_SERVICE_FQN)) {
continue;
}
// Make sure subclass isn't just defining another abstract definition
// of the method
if ((method.getModifiers() & Modifier.ABSTRACT) != 0) {
continue;
}
// Make sure it has the exact right signature
if (method.getArgumentCount() != 1) {
continue; // Wrong signature
}
if (!method.getArgumentType(0).matchesName(STATUS_BAR_NOTIFICATION_FQN)) {
continue;
}
found = true;
break;
}
if (!found) {
String message = String.format(
"Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d",
NOTIFICATION_LISTENER_SERVICE_FQN, methodName,
STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk);
Node nameNode = node.astName();
context.report(ISSUE, node, context.getLocation(nameNode),
message);
break;
}
}
}
private static int getTargetApi(ClassDeclaration node) {
while (node != null) {
int targetApi = ApiDetector.getTargetApi(node.astModifiers());
if (targetApi != -1) {
return targetApi;
}
node = JavaContext.findSurroundingClass(node.getParent());
}
return -1;
}
}