blob: 02e4654b03ed9f3baaf03f9ce289c5787fad1c1a [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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UastUtils;
/**
* 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 SourceCodeScanner {
/** Are previously-abstract methods all overridden? */
public static final Issue ISSUE =
Issue.create(
"OverrideAbstract",
"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.ERROR,
new Implementation(
OverrideConcreteDetector.class, Scope.JAVA_FILE_SCOPE))
.setAndroidSpecific(true);
// 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 SourceCodeScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN);
}
@Override
public void visitClass(@NonNull JavaContext context, @NonNull UClass declaration) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(declaration)) {
return;
}
int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(declaration));
if (minSdk >= CONCRETE_IN) {
return;
}
String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED};
for (String methodName : methodNames) {
boolean found = false;
for (PsiMethod method : declaration.findMethodsByName(methodName, true)) {
// Make sure it's not the base method, but that it's been defined
// in a subclass, concretely
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
continue;
}
if (NOTIFICATION_LISTENER_SERVICE_FQN.equals(containingClass.getQualifiedName())) {
continue;
}
// Make sure subclass isn't just defining another abstract definition
// of the method
if (evaluator.isAbstract(method)) {
continue;
}
// Make sure it has the exact right signature
if (method.getParameterList().getParametersCount() != 1) {
continue; // Wrong signature
}
if (!evaluator.parameterHasType(method, 0, 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);
context.report(ISSUE, declaration, context.getNameLocation(declaration), message);
break;
}
}
}
private static int getTargetApi(@NonNull UClass node) {
while (node != null) {
int targetApi = ApiDetector.getTargetApi(node);
if (targetApi != -1) {
return targetApi;
}
node = UastUtils.getParentOfType(node, UClass.class, true);
}
return -1;
}
}