blob: 9e4a4079d8ec8ede9ce4e98bf1ead084728917de [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 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.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
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.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 com.google.common.collect.Maps;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.Cast;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.InlineIfExpression;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Looks for addJavascriptInterface calls on interfaces have been properly annotated
* with {@code @JavaScriptInterface}
*/
public class JavaScriptInterfaceDetector extends Detector implements Detector.JavaScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"JavascriptInterface", //$NON-NLS-1$
"Missing @JavascriptInterface on methods",
"As of API 17, you must annotate methods in objects registered with the " +
"`addJavascriptInterface` method with a `@JavascriptInterface` annotation.",
Category.SECURITY,
8,
Severity.ERROR,
new Implementation(
JavaScriptInterfaceDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$
private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$
private static final String JAVASCRIPT_INTERFACE_CLS = "android.webkit.JavascriptInterface"; //$NON-NLS-1$
private static final String WEB_VIEW_CLS = "android.webkit.WebView"; //$NON-NLS-1$
/** Constructs a new {@link JavaScriptInterfaceDetector} check */
public JavaScriptInterfaceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW; // because it relies on class loading referenced javascript interface
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation call) {
if (context.getMainProject().getTargetSdk() < 17) {
return;
}
if (call.astArguments().size() != 2) {
return;
}
if (!isCallOnWebView(context, call)) {
return;
}
Expression first = call.astArguments().first();
ResolvedNode resolved = context.resolve(first);
if (resolved instanceof ResolvedVariable) {
// We're passing in a variable to the addJavaScriptInterface method;
// the variable may be of a more generic type than the actual
// value assigned to it. For example, we may have a scenario like this:
// Object object = new SpecificType();
// addJavaScriptInterface(object, ...)
// Here the type of the variable is Object, but we know that it can
// contain objects of type SpecificType, so we should check that type instead.
Node method = JavaContext.findSurroundingMethod(call);
if (method != null) {
ConcreteTypeVisitor v = new ConcreteTypeVisitor(context, call);
method.accept(v);
resolved = v.getType();
if (resolved == null) {
return;
}
} else {
return;
}
} else if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.isConstructor()) {
resolved = method.getContainingClass();
} else {
TypeDescriptor returnType = method.getReturnType();
if (returnType != null) {
resolved = returnType.getTypeClass();
}
}
} else {
TypeDescriptor type = context.getType(first);
if (type != null) {
resolved = type.getTypeClass();
}
}
if (resolved instanceof ResolvedClass) {
ResolvedClass cls = (ResolvedClass) resolved;
if (isJavaScriptAnnotated(cls)) {
return;
}
Location location = context.getLocation(call.astName());
String message = String.format(
"None of the methods in the added interface (%1$s) have been annotated " +
"with `@android.webkit.JavascriptInterface`; they will not " +
"be visible in API 17", cls.getSimpleName());
context.report(ISSUE, call, location, message);
}
}
private static boolean isCallOnWebView(JavaContext context, MethodInvocation call) {
ResolvedNode resolved = context.resolve(call);
if (!(resolved instanceof ResolvedMethod)) {
return false;
}
ResolvedMethod method = (ResolvedMethod) resolved;
return method.getContainingClass().matches(WEB_VIEW_CLS);
}
private static boolean isJavaScriptAnnotated(ResolvedClass clz) {
while (clz != null) {
for (ResolvedAnnotation annotation : clz.getAnnotations()) {
if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) {
return true;
}
}
for (ResolvedMethod method : clz.getMethods(false)) {
for (ResolvedAnnotation annotation : method.getAnnotations()) {
if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) {
return true;
}
}
}
clz = clz.getSuperClass();
}
return false;
}
private static class ConcreteTypeVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final MethodInvocation mTargetCall;
private boolean mFoundCall;
private Map<Node, ResolvedClass> mTypes = Maps.newIdentityHashMap();
private Map<ResolvedVariable, ResolvedClass> mVariableTypes = Maps.newHashMap();
public ConcreteTypeVisitor(JavaContext context, MethodInvocation call) {
mContext = context;
mTargetCall = call;
}
public ResolvedClass getType() {
Expression first = mTargetCall.astArguments().first();
ResolvedClass resolvedClass = mTypes.get(first);
if (resolvedClass == null) {
ResolvedNode resolved = mContext.resolve(first);
if (resolved instanceof ResolvedVariable) {
resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass == null) {
return ((ResolvedVariable)resolved).getType().getTypeClass();
}
}
}
return resolvedClass;
}
@Override
public boolean visitNode(Node node) {
return mFoundCall || super.visitNode(node);
}
@Override
public void afterVisitMethodInvocation(MethodInvocation node) {
if (node == mTargetCall) {
mFoundCall = true;
}
}
@Override
public void afterVisitConstructorInvocation(@NonNull ConstructorInvocation node) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
mTypes.put(node, method.getContainingClass());
} else {
// Implicit constructor?
TypeDescriptor type = mContext.getType(node);
if (type != null) {
ResolvedClass typeClass = type.getTypeClass();
if (typeClass != null) {
mTypes.put(node, typeClass);
}
}
}
}
@Override
public void afterVisitVariableReference(VariableReference node) {
if (mTypes.get(node) == null) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedVariable) {
ResolvedClass resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}
@Override
public void afterVisitBinaryExpression(BinaryExpression node) {
if (node.astOperator() == BinaryOperator.ASSIGN) {
Expression rhs = node.astRight();
ResolvedClass resolvedClass = mTypes.get(rhs);
if (resolvedClass != null) {
Expression lhs = node.astLeft();
mTypes.put(lhs, resolvedClass);
ResolvedNode variable = mContext.resolve(lhs);
if (variable instanceof ResolvedVariable) {
mVariableTypes.put((ResolvedVariable) variable, resolvedClass);
}
}
}
}
@Override
public void afterVisitInlineIfExpression(InlineIfExpression node) {
ResolvedClass resolvedClass = mTypes.get(node.astIfTrue());
if (resolvedClass == null) {
resolvedClass = mTypes.get(node.astIfFalse());
}
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
@Override
public void afterVisitVariableDefinitionEntry(VariableDefinitionEntry node) {
Expression initializer = node.astInitializer();
if (initializer != null) {
ResolvedClass resolvedClass = mTypes.get(initializer);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
ResolvedNode variable = mContext.resolve(node);
if (variable instanceof ResolvedVariable) {
mVariableTypes.put((ResolvedVariable) variable, resolvedClass);
}
}
}
}
@Override
public void afterVisitCast(Cast node) {
ResolvedClass resolvedClass = mTypes.get(node);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}