as31: Drop custom Kotlin Android lint, register quickfixes with extension point
This commit is contained in:
committed by
Nikolay Krasko
parent
686d8a1756
commit
11ee744340
File diff suppressed because it is too large
Load Diff
-690
@@ -1,690 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.klint.detector.api;
|
||||
|
||||
import static com.android.SdkConstants.ANDROID_PKG;
|
||||
import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
|
||||
import static com.android.SdkConstants.CLASS_CONTEXT;
|
||||
import static com.android.SdkConstants.CLASS_FRAGMENT;
|
||||
import static com.android.SdkConstants.CLASS_RESOURCES;
|
||||
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
|
||||
import static com.android.SdkConstants.R_CLASS;
|
||||
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
|
||||
import static com.android.tools.klint.client.api.UastLintUtils.toAndroidReferenceViaResolve;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.resources.ResourceUrl;
|
||||
import com.android.resources.ResourceType;
|
||||
import com.android.tools.klint.client.api.AndroidReference;
|
||||
import com.android.tools.klint.client.api.JavaEvaluator;
|
||||
import com.android.tools.klint.client.api.UastLintUtils;
|
||||
import com.intellij.psi.PsiAnnotation;
|
||||
import com.intellij.psi.PsiAssignmentExpression;
|
||||
import com.intellij.psi.PsiClass;
|
||||
import com.intellij.psi.PsiConditionalExpression;
|
||||
import com.intellij.psi.PsiDeclarationStatement;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiExpression;
|
||||
import com.intellij.psi.PsiExpressionStatement;
|
||||
import com.intellij.psi.PsiField;
|
||||
import com.intellij.psi.PsiLocalVariable;
|
||||
import com.intellij.psi.PsiMethod;
|
||||
import com.intellij.psi.PsiMethodCallExpression;
|
||||
import com.intellij.psi.PsiModifierListOwner;
|
||||
import com.intellij.psi.PsiParameter;
|
||||
import com.intellij.psi.PsiParenthesizedExpression;
|
||||
import com.intellij.psi.PsiReference;
|
||||
import com.intellij.psi.PsiReferenceExpression;
|
||||
import com.intellij.psi.PsiStatement;
|
||||
import com.intellij.psi.PsiVariable;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
|
||||
import org.jetbrains.uast.UCallExpression;
|
||||
import org.jetbrains.uast.UElement;
|
||||
import org.jetbrains.uast.UExpression;
|
||||
import org.jetbrains.uast.UIfExpression;
|
||||
import org.jetbrains.uast.UParenthesizedExpression;
|
||||
import org.jetbrains.uast.UQualifiedReferenceExpression;
|
||||
import org.jetbrains.uast.UastUtils;
|
||||
import org.jetbrains.uast.UReferenceExpression;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Evaluates constant expressions */
|
||||
public class ResourceEvaluator {
|
||||
|
||||
/**
|
||||
* Marker ResourceType used to signify that an expression is of type {@code @ColorInt},
|
||||
* which isn't actually a ResourceType but one we want to specifically compare with.
|
||||
* We're using {@link ResourceType#PUBLIC} because that one won't appear in the R
|
||||
* class (and ResourceType is an enum we can't just create new constants for.)
|
||||
*/
|
||||
public static final ResourceType COLOR_INT_MARKER_TYPE = ResourceType.PUBLIC;
|
||||
/**
|
||||
* Marker ResourceType used to signify that an expression is of type {@code @Px},
|
||||
* which isn't actually a ResourceType but one we want to specifically compare with.
|
||||
* We're using {@link ResourceType#DECLARE_STYLEABLE} because that one doesn't
|
||||
* have a corresponding {@code *Res} constant (and ResourceType is an enum we can't
|
||||
* just create new constants for.)
|
||||
*/
|
||||
public static final ResourceType PX_MARKER_TYPE = ResourceType.DECLARE_STYLEABLE;
|
||||
|
||||
public static final String COLOR_INT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "ColorInt"; //$NON-NLS-1$
|
||||
public static final String PX_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Px"; //$NON-NLS-1$
|
||||
public static final String RES_SUFFIX = "Res";
|
||||
|
||||
public static final String CLS_TYPED_ARRAY = "android.content.res.TypedArray";
|
||||
|
||||
private final JavaContext mContext;
|
||||
private final JavaEvaluator mEvaluator;
|
||||
|
||||
private boolean mAllowDereference = true;
|
||||
|
||||
/**
|
||||
* Creates a new resource evaluator
|
||||
*
|
||||
* @param context Java context
|
||||
*/
|
||||
public ResourceEvaluator(JavaContext context) {
|
||||
mContext = context;
|
||||
mEvaluator = context.getEvaluator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we allow dereferencing resources when computing constants;
|
||||
* e.g. if we ask for the resource for {@code x} when the code is
|
||||
* {@code x = getString(R.string.name)}, if {@code allowDereference} is
|
||||
* true we'll return R.string.name, otherwise we'll return null.
|
||||
*
|
||||
* @return this for constructor chaining
|
||||
*/
|
||||
public ResourceEvaluator allowDereference(boolean allow) {
|
||||
mAllowDereference = allow;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource reference (type and name) it
|
||||
* points to, if any
|
||||
*
|
||||
* @param context Java context
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding resource url (type and name)
|
||||
*/
|
||||
@Nullable
|
||||
public static ResourceUrl getResource(
|
||||
@NonNull JavaContext context,
|
||||
@NonNull PsiElement element) {
|
||||
return new ResourceEvaluator(context).getResource(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource reference (type and name) it
|
||||
* points to, if any
|
||||
*
|
||||
* @param context Java context
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding resource url (type and name)
|
||||
*/
|
||||
@Nullable
|
||||
public static ResourceUrl getResource(
|
||||
@NonNull JavaContext context,
|
||||
@NonNull UElement element) {
|
||||
return new ResourceEvaluator(context).getResource(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource types implied by the given element,
|
||||
* if any.
|
||||
*
|
||||
* @param context Java context
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding resource types
|
||||
*/
|
||||
@Nullable
|
||||
public static EnumSet<ResourceType> getResourceTypes(
|
||||
@NonNull JavaContext context,
|
||||
@NonNull PsiElement element) {
|
||||
return new ResourceEvaluator(context).getResourceTypes(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource types implied by the given element,
|
||||
* if any.
|
||||
*
|
||||
* @param context Java context
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding resource types
|
||||
*/
|
||||
@Nullable
|
||||
public static EnumSet<ResourceType> getResourceTypes(
|
||||
@NonNull JavaContext context,
|
||||
@NonNull UElement element) {
|
||||
return new ResourceEvaluator(context).getResourceTypes(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource reference (type and name) it
|
||||
* points to, if any
|
||||
*
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding constant value - a String, an Integer, a Float, and so on
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceUrl getResource(@Nullable UElement element) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element instanceof UIfExpression) {
|
||||
UIfExpression expression = (UIfExpression) element;
|
||||
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
|
||||
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
|
||||
return getResource(expression.getThenExpression());
|
||||
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
|
||||
return getResource(expression.getElseExpression());
|
||||
}
|
||||
} else if (element instanceof UParenthesizedExpression) {
|
||||
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
|
||||
return getResource(parenthesizedExpression.getExpression());
|
||||
} else if (mAllowDereference && element instanceof UQualifiedReferenceExpression) {
|
||||
UQualifiedReferenceExpression qualifiedExpression = (UQualifiedReferenceExpression) element;
|
||||
UExpression selector = qualifiedExpression.getSelector();
|
||||
if ((selector instanceof UCallExpression)) {
|
||||
UCallExpression call = (UCallExpression) selector;
|
||||
PsiMethod function = call.resolve();
|
||||
PsiClass containingClass = UastUtils.getContainingClass(function);
|
||||
if (function != null && containingClass != null) {
|
||||
String qualifiedName = containingClass.getQualifiedName();
|
||||
String name = call.getMethodName();
|
||||
if ((CLASS_RESOURCES.equals(qualifiedName)
|
||||
|| CLASS_CONTEXT.equals(qualifiedName)
|
||||
|| CLASS_FRAGMENT.equals(qualifiedName)
|
||||
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|
||||
|| CLS_TYPED_ARRAY.equals(qualifiedName))
|
||||
&& name != null
|
||||
&& name.startsWith("get")) {
|
||||
List<UExpression> args = call.getValueArguments();
|
||||
if (!args.isEmpty()) {
|
||||
return getResource(args.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof UReferenceExpression) {
|
||||
ResourceUrl url = getResourceConstant(element);
|
||||
if (url != null) {
|
||||
return url;
|
||||
}
|
||||
PsiElement resolved = ((UReferenceExpression) element).resolve();
|
||||
if (resolved instanceof PsiVariable) {
|
||||
PsiVariable variable = (PsiVariable) resolved;
|
||||
UElement lastAssignment =
|
||||
UastLintUtils.findLastAssignment(
|
||||
variable, element, mContext);
|
||||
|
||||
if (lastAssignment != null) {
|
||||
return getResource(lastAssignment);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource reference (type and name) it
|
||||
* points to, if any
|
||||
*
|
||||
* @param element the node to compute the constant value for
|
||||
* @return the corresponding constant value - a String, an Integer, a Float, and so on
|
||||
*/
|
||||
|
||||
@Nullable
|
||||
public ResourceUrl getResource(@Nullable PsiElement element) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
if (element instanceof PsiConditionalExpression) {
|
||||
PsiConditionalExpression expression = (PsiConditionalExpression) element;
|
||||
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
|
||||
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
|
||||
return getResource(expression.getThenExpression());
|
||||
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
|
||||
return getResource(expression.getElseExpression());
|
||||
}
|
||||
} else if (element instanceof PsiParenthesizedExpression) {
|
||||
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
|
||||
return getResource(parenthesizedExpression.getExpression());
|
||||
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
|
||||
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
|
||||
PsiReferenceExpression expression = call.getMethodExpression();
|
||||
PsiMethod method = call.resolveMethod();
|
||||
if (method != null && method.getContainingClass() != null) {
|
||||
String qualifiedName = method.getContainingClass().getQualifiedName();
|
||||
String name = expression.getReferenceName();
|
||||
if ((CLASS_RESOURCES.equals(qualifiedName)
|
||||
|| CLASS_CONTEXT.equals(qualifiedName)
|
||||
|| CLASS_FRAGMENT.equals(qualifiedName)
|
||||
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|
||||
|| CLS_TYPED_ARRAY.equals(qualifiedName))
|
||||
&& name != null
|
||||
&& name.startsWith("get")) {
|
||||
PsiExpression[] args = call.getArgumentList().getExpressions();
|
||||
if (args.length > 0) {
|
||||
return getResource(args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element instanceof PsiReference) {
|
||||
ResourceUrl url = getResourceConstant(element);
|
||||
if (url != null) {
|
||||
return url;
|
||||
}
|
||||
PsiElement resolved = ((PsiReference) element).resolve();
|
||||
if (resolved instanceof PsiField) {
|
||||
url = getResourceConstant(resolved);
|
||||
if (url != null) {
|
||||
return url;
|
||||
}
|
||||
PsiField field = (PsiField) resolved;
|
||||
if (field.getInitializer() != null) {
|
||||
return getResource(field.getInitializer());
|
||||
}
|
||||
return null;
|
||||
} else if (resolved instanceof PsiLocalVariable) {
|
||||
PsiLocalVariable variable = (PsiLocalVariable) resolved;
|
||||
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
|
||||
false);
|
||||
if (statement != null) {
|
||||
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
|
||||
PsiStatement.class);
|
||||
String targetName = variable.getName();
|
||||
if (targetName == null) {
|
||||
return null;
|
||||
}
|
||||
while (prev != null) {
|
||||
if (prev instanceof PsiDeclarationStatement) {
|
||||
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
|
||||
for (PsiElement e : prevStatement.getDeclaredElements()) {
|
||||
if (variable.equals(e)) {
|
||||
return getResource(variable.getInitializer());
|
||||
}
|
||||
}
|
||||
} else if (prev instanceof PsiExpressionStatement) {
|
||||
PsiExpression expression = ((PsiExpressionStatement) prev)
|
||||
.getExpression();
|
||||
if (expression instanceof PsiAssignmentExpression) {
|
||||
PsiAssignmentExpression assign
|
||||
= (PsiAssignmentExpression) expression;
|
||||
PsiExpression lhs = assign.getLExpression();
|
||||
if (lhs instanceof PsiReferenceExpression) {
|
||||
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
|
||||
if (targetName.equals(reference.getReferenceName()) &&
|
||||
reference.getQualifier() == null) {
|
||||
return getResource(assign.getRExpression());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
|
||||
PsiStatement.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource types applicable to the
|
||||
* node, if any.
|
||||
*
|
||||
* @param element the element to compute the types for
|
||||
* @return the corresponding resource types
|
||||
*/
|
||||
@Nullable
|
||||
public EnumSet<ResourceType> getResourceTypes(@Nullable UElement element) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
if (element instanceof UIfExpression) {
|
||||
UIfExpression expression = (UIfExpression) element;
|
||||
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
|
||||
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
|
||||
return getResourceTypes(expression.getThenExpression());
|
||||
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
|
||||
return getResourceTypes(expression.getElseExpression());
|
||||
} else {
|
||||
EnumSet<ResourceType> left = getResourceTypes(
|
||||
expression.getThenExpression());
|
||||
EnumSet<ResourceType> right = getResourceTypes(
|
||||
expression.getElseExpression());
|
||||
if (left == null) {
|
||||
return right;
|
||||
} else if (right == null) {
|
||||
return left;
|
||||
} else {
|
||||
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
|
||||
copy.addAll(right);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
} else if (element instanceof UParenthesizedExpression) {
|
||||
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
|
||||
return getResourceTypes(parenthesizedExpression.getExpression());
|
||||
} else if ((element instanceof UQualifiedReferenceExpression && mAllowDereference)
|
||||
|| element instanceof UCallExpression) {
|
||||
UElement probablyCallExpression = element;
|
||||
if (element instanceof UQualifiedReferenceExpression) {
|
||||
UQualifiedReferenceExpression qualifiedExpression =
|
||||
(UQualifiedReferenceExpression) element;
|
||||
probablyCallExpression = qualifiedExpression.getSelector();
|
||||
}
|
||||
if ((probablyCallExpression instanceof UCallExpression)) {
|
||||
UCallExpression call = (UCallExpression) probablyCallExpression;
|
||||
PsiMethod method = call.resolve();
|
||||
PsiClass containingClass = UastUtils.getContainingClass(method);
|
||||
if (method != null && containingClass != null) {
|
||||
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
|
||||
if (types != null) {
|
||||
return types;
|
||||
}
|
||||
|
||||
String qualifiedName = containingClass.getQualifiedName();
|
||||
String name = call.getMethodName();
|
||||
if ((CLASS_RESOURCES.equals(qualifiedName)
|
||||
|| CLASS_CONTEXT.equals(qualifiedName)
|
||||
|| CLASS_FRAGMENT.equals(qualifiedName)
|
||||
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|
||||
|| CLS_TYPED_ARRAY.equals(qualifiedName))
|
||||
&& name != null
|
||||
&& name.startsWith("get")) {
|
||||
List<UExpression> args = call.getValueArguments();
|
||||
if (!args.isEmpty()) {
|
||||
types = getResourceTypes(args.get(0));
|
||||
if (types != null) {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof UReferenceExpression) {
|
||||
ResourceUrl url = getResourceConstant(element);
|
||||
if (url != null) {
|
||||
return EnumSet.of(url.type);
|
||||
}
|
||||
|
||||
PsiElement resolved = ((UReferenceExpression) element).resolve();
|
||||
if (resolved instanceof PsiVariable) {
|
||||
PsiVariable variable = (PsiVariable) resolved;
|
||||
UElement lastAssignment =
|
||||
UastLintUtils.findLastAssignment(variable, element, mContext);
|
||||
|
||||
if (lastAssignment != null) {
|
||||
return getResourceTypes(lastAssignment);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given node and returns the resource types applicable to the
|
||||
* node, if any.
|
||||
*
|
||||
* @param element the element to compute the types for
|
||||
* @return the corresponding resource types
|
||||
*/
|
||||
@Nullable
|
||||
public EnumSet<ResourceType> getResourceTypes(@Nullable PsiElement element) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
if (element instanceof PsiConditionalExpression) {
|
||||
PsiConditionalExpression expression = (PsiConditionalExpression) element;
|
||||
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
|
||||
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
|
||||
return getResourceTypes(expression.getThenExpression());
|
||||
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
|
||||
return getResourceTypes(expression.getElseExpression());
|
||||
} else {
|
||||
EnumSet<ResourceType> left = getResourceTypes(
|
||||
expression.getThenExpression());
|
||||
EnumSet<ResourceType> right = getResourceTypes(
|
||||
expression.getElseExpression());
|
||||
if (left == null) {
|
||||
return right;
|
||||
} else if (right == null) {
|
||||
return left;
|
||||
} else {
|
||||
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
|
||||
copy.addAll(right);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
} else if (element instanceof PsiParenthesizedExpression) {
|
||||
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
|
||||
return getResourceTypes(parenthesizedExpression.getExpression());
|
||||
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
|
||||
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
|
||||
PsiReferenceExpression expression = call.getMethodExpression();
|
||||
PsiMethod method = call.resolveMethod();
|
||||
if (method != null && method.getContainingClass() != null) {
|
||||
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
|
||||
if (types != null) {
|
||||
return types;
|
||||
}
|
||||
|
||||
String qualifiedName = method.getContainingClass().getQualifiedName();
|
||||
String name = expression.getReferenceName();
|
||||
if ((CLASS_RESOURCES.equals(qualifiedName)
|
||||
|| CLASS_CONTEXT.equals(qualifiedName)
|
||||
|| CLASS_FRAGMENT.equals(qualifiedName)
|
||||
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|
||||
|| CLS_TYPED_ARRAY.equals(qualifiedName))
|
||||
&& name != null
|
||||
&& name.startsWith("get")) {
|
||||
PsiExpression[] args = call.getArgumentList().getExpressions();
|
||||
if (args.length > 0) {
|
||||
types = getResourceTypes(args[0]);
|
||||
if (types != null) {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element instanceof PsiReference) {
|
||||
ResourceUrl url = getResourceConstant(element);
|
||||
if (url != null) {
|
||||
return EnumSet.of(url.type);
|
||||
}
|
||||
PsiElement resolved = ((PsiReference) element).resolve();
|
||||
if (resolved instanceof PsiField) {
|
||||
url = getResourceConstant(resolved);
|
||||
if (url != null) {
|
||||
return EnumSet.of(url.type);
|
||||
}
|
||||
PsiField field = (PsiField) resolved;
|
||||
if (field.getInitializer() != null) {
|
||||
return getResourceTypes(field.getInitializer());
|
||||
}
|
||||
return null;
|
||||
} else if (resolved instanceof PsiParameter) {
|
||||
return getTypesFromAnnotations((PsiParameter)resolved);
|
||||
} else if (resolved instanceof PsiLocalVariable) {
|
||||
PsiLocalVariable variable = (PsiLocalVariable) resolved;
|
||||
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
|
||||
false);
|
||||
if (statement != null) {
|
||||
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
|
||||
PsiStatement.class);
|
||||
String targetName = variable.getName();
|
||||
if (targetName == null) {
|
||||
return null;
|
||||
}
|
||||
while (prev != null) {
|
||||
if (prev instanceof PsiDeclarationStatement) {
|
||||
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
|
||||
for (PsiElement e : prevStatement.getDeclaredElements()) {
|
||||
if (variable.equals(e)) {
|
||||
return getResourceTypes(variable.getInitializer());
|
||||
}
|
||||
}
|
||||
} else if (prev instanceof PsiExpressionStatement) {
|
||||
PsiExpression expression = ((PsiExpressionStatement) prev)
|
||||
.getExpression();
|
||||
if (expression instanceof PsiAssignmentExpression) {
|
||||
PsiAssignmentExpression assign
|
||||
= (PsiAssignmentExpression) expression;
|
||||
PsiExpression lhs = assign.getLExpression();
|
||||
if (lhs instanceof PsiReferenceExpression) {
|
||||
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
|
||||
if (targetName.equals(reference.getReferenceName()) &&
|
||||
reference.getQualifier() == null) {
|
||||
return getResourceTypes(assign.getRExpression());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
|
||||
PsiStatement.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private EnumSet<ResourceType> getTypesFromAnnotations(PsiModifierListOwner owner) {
|
||||
if (mEvaluator == null) {
|
||||
return null;
|
||||
}
|
||||
for (PsiAnnotation annotation : mEvaluator.getAllAnnotations(owner)) {
|
||||
String signature = annotation.getQualifiedName();
|
||||
if (signature == null) {
|
||||
continue;
|
||||
}
|
||||
if (signature.equals(COLOR_INT_ANNOTATION)) {
|
||||
return EnumSet.of(COLOR_INT_MARKER_TYPE);
|
||||
}
|
||||
if (signature.equals(PX_ANNOTATION)) {
|
||||
return EnumSet.of(PX_MARKER_TYPE);
|
||||
}
|
||||
if (signature.endsWith(RES_SUFFIX)
|
||||
&& signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
|
||||
String typeString = signature
|
||||
.substring(SUPPORT_ANNOTATIONS_PREFIX.length(),
|
||||
signature.length() - RES_SUFFIX.length())
|
||||
.toLowerCase(Locale.US);
|
||||
ResourceType type = ResourceType.getEnum(typeString);
|
||||
if (type != null) {
|
||||
return EnumSet.of(type);
|
||||
} else if (typeString.equals("any")) { // @AnyRes
|
||||
return getAnyRes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns a resource URL based on the field reference in the code */
|
||||
@Nullable
|
||||
public static ResourceUrl getResourceConstant(@NonNull PsiElement node) {
|
||||
// R.type.name
|
||||
if (node instanceof PsiReferenceExpression) {
|
||||
PsiReferenceExpression expression = (PsiReferenceExpression) node;
|
||||
if (expression.getQualifier() instanceof PsiReferenceExpression) {
|
||||
PsiReferenceExpression select = (PsiReferenceExpression) expression.getQualifier();
|
||||
if (select.getQualifier() instanceof PsiReferenceExpression) {
|
||||
PsiReferenceExpression reference = (PsiReferenceExpression) select
|
||||
.getQualifier();
|
||||
if (R_CLASS.equals(reference.getReferenceName())) {
|
||||
String typeName = select.getReferenceName();
|
||||
String name = expression.getReferenceName();
|
||||
|
||||
ResourceType type = ResourceType.getEnum(typeName);
|
||||
if (type != null && name != null) {
|
||||
boolean isFramework =
|
||||
reference.getQualifier() instanceof PsiReferenceExpression
|
||||
&& ANDROID_PKG
|
||||
.equals(((PsiReferenceExpression) reference.
|
||||
getQualifier()).getReferenceName());
|
||||
|
||||
return ResourceUrl.create(type, name, isFramework);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (node instanceof PsiField) {
|
||||
PsiField field = (PsiField) node;
|
||||
PsiClass typeClass = field.getContainingClass();
|
||||
if (typeClass != null) {
|
||||
PsiClass rClass = typeClass.getContainingClass();
|
||||
if (rClass != null && R_CLASS.equals(rClass.getName())) {
|
||||
String name = field.getName();
|
||||
ResourceType type = ResourceType.getEnum(typeClass.getName());
|
||||
if (type != null && name != null) {
|
||||
String qualifiedName = rClass.getQualifiedName();
|
||||
boolean isFramework = qualifiedName != null
|
||||
&& qualifiedName.startsWith(ANDROID_PKG_PREFIX);
|
||||
return ResourceUrl.create(type, name, isFramework);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns a resource URL based on the field reference in the code */
|
||||
@Nullable
|
||||
public static ResourceUrl getResourceConstant(@NonNull UElement node) {
|
||||
AndroidReference androidReference = toAndroidReferenceViaResolve(node);
|
||||
if (androidReference == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = androidReference.getName();
|
||||
ResourceType type = androidReference.getType();
|
||||
boolean isFramework = androidReference.getPackage().equals("android");
|
||||
|
||||
return ResourceUrl.create(type, name, isFramework);
|
||||
}
|
||||
|
||||
private static EnumSet<ResourceType> getAnyRes() {
|
||||
EnumSet<ResourceType> types = EnumSet.allOf(ResourceType.class);
|
||||
types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE);
|
||||
types.remove(ResourceEvaluator.PX_MARKER_TYPE);
|
||||
return types;
|
||||
}
|
||||
}
|
||||
|
||||
-744
@@ -1,744 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.klint.checks;
|
||||
|
||||
import static com.android.SdkConstants.ANDROID_URI;
|
||||
import static com.android.SdkConstants.ATTR_EXPORTED;
|
||||
import static com.android.SdkConstants.ATTR_HOST;
|
||||
import static com.android.SdkConstants.ATTR_PATH;
|
||||
import static com.android.SdkConstants.ATTR_PATH_PREFIX;
|
||||
import static com.android.SdkConstants.ATTR_SCHEME;
|
||||
import static com.android.SdkConstants.CLASS_ACTIVITY;
|
||||
import static com.android.xml.AndroidManifest.ATTRIBUTE_MIME_TYPE;
|
||||
import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
|
||||
import static com.android.xml.AndroidManifest.ATTRIBUTE_PORT;
|
||||
import static com.android.xml.AndroidManifest.NODE_ACTION;
|
||||
import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
|
||||
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
|
||||
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
|
||||
import static com.android.xml.AndroidManifest.NODE_DATA;
|
||||
import static com.android.xml.AndroidManifest.NODE_INTENT;
|
||||
import static com.android.xml.AndroidManifest.NODE_MANIFEST;
|
||||
|
||||
import com.android.SdkConstants;
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.ide.common.rendering.api.ResourceValue;
|
||||
import com.android.ide.common.res2.AbstractResourceRepository;
|
||||
import com.android.ide.common.res2.ResourceItem;
|
||||
import com.android.resources.ResourceUrl;
|
||||
import com.android.resources.ResourceType;
|
||||
import com.android.tools.klint.client.api.JavaEvaluator;
|
||||
import com.android.tools.klint.client.api.LintClient;
|
||||
import com.android.tools.klint.client.api.XmlParser;
|
||||
import com.android.tools.klint.detector.api.Category;
|
||||
import com.android.tools.klint.detector.api.Context;
|
||||
import com.android.tools.klint.detector.api.Detector;
|
||||
import com.android.tools.klint.detector.api.Detector.XmlScanner;
|
||||
import com.android.tools.klint.detector.api.Implementation;
|
||||
import com.android.tools.klint.detector.api.Issue;
|
||||
import com.android.tools.klint.detector.api.JavaContext;
|
||||
import com.android.tools.klint.detector.api.LintUtils;
|
||||
import com.android.tools.klint.detector.api.Project;
|
||||
import com.android.tools.klint.detector.api.Scope;
|
||||
import com.android.tools.klint.detector.api.Severity;
|
||||
import com.android.tools.klint.detector.api.XmlContext;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiField;
|
||||
|
||||
import com.intellij.psi.util.InheritanceUtil;
|
||||
import org.jetbrains.uast.UCallExpression;
|
||||
import org.jetbrains.uast.UClass;
|
||||
import org.jetbrains.uast.UElement;
|
||||
import org.jetbrains.uast.UExpression;
|
||||
import org.jetbrains.uast.UastUtils;
|
||||
import org.jetbrains.uast.util.UastExpressionUtils;
|
||||
import org.jetbrains.uast.visitor.AbstractUastVisitor;
|
||||
import org.w3c.dom.Attr;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* Check if the usage of App Indexing is correct.
|
||||
*/
|
||||
public class AppIndexingApiDetector extends Detector implements XmlScanner, Detector.UastScanner {
|
||||
|
||||
private static final Implementation URL_IMPLEMENTATION = new Implementation(
|
||||
AppIndexingApiDetector.class, Scope.MANIFEST_SCOPE);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Implementation APP_INDEXING_API_IMPLEMENTATION =
|
||||
new Implementation(
|
||||
AppIndexingApiDetector.class,
|
||||
EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST),
|
||||
Scope.JAVA_FILE_SCOPE, Scope.MANIFEST_SCOPE);
|
||||
|
||||
public static final Issue ISSUE_URL_ERROR = Issue.create(
|
||||
"GoogleAppIndexingUrlError", //$NON-NLS-1$
|
||||
"URL not supported by app for Google App Indexing",
|
||||
"Ensure the URL is supported by your app, to get installs and traffic to your"
|
||||
+ " app from Google Search.",
|
||||
Category.USABILITY, 5, Severity.ERROR, URL_IMPLEMENTATION)
|
||||
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
|
||||
|
||||
public static final Issue ISSUE_APP_INDEXING =
|
||||
Issue.create(
|
||||
"GoogleAppIndexingWarning", //$NON-NLS-1$
|
||||
"Missing support for Google App Indexing",
|
||||
"Adds URLs to get your app into the Google index, to get installs"
|
||||
+ " and traffic to your app from Google Search.",
|
||||
Category.USABILITY, 5, Severity.WARNING, URL_IMPLEMENTATION)
|
||||
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
|
||||
|
||||
public static final Issue ISSUE_APP_INDEXING_API =
|
||||
Issue.create(
|
||||
"GoogleAppIndexingApiWarning", //$NON-NLS-1$
|
||||
"Missing support for Google App Indexing Api",
|
||||
"Adds URLs to get your app into the Google index, to get installs"
|
||||
+ " and traffic to your app from Google Search.",
|
||||
Category.USABILITY, 5, Severity.WARNING, APP_INDEXING_API_IMPLEMENTATION)
|
||||
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio")
|
||||
.setEnabledByDefault(false);
|
||||
|
||||
private static final String[] PATH_ATTR_LIST = new String[]{ATTR_PATH_PREFIX, ATTR_PATH};
|
||||
private static final String SCHEME_MISSING = "android:scheme is missing";
|
||||
private static final String HOST_MISSING = "android:host is missing";
|
||||
private static final String DATA_MISSING = "Missing data element";
|
||||
private static final String URL_MISSING = "Missing URL for the intent filter";
|
||||
private static final String NOT_BROWSABLE
|
||||
= "Activity supporting ACTION_VIEW is not set as BROWSABLE";
|
||||
private static final String ILLEGAL_NUMBER = "android:port is not a legal number";
|
||||
|
||||
private static final String APP_INDEX_START = "start"; //$NON-NLS-1$
|
||||
private static final String APP_INDEX_END = "end"; //$NON-NLS-1$
|
||||
private static final String APP_INDEX_VIEW = "view"; //$NON-NLS-1$
|
||||
private static final String APP_INDEX_VIEW_END = "viewEnd"; //$NON-NLS-1$
|
||||
private static final String CLIENT_CONNECT = "connect"; //$NON-NLS-1$
|
||||
private static final String CLIENT_DISCONNECT = "disconnect"; //$NON-NLS-1$
|
||||
private static final String ADD_API = "addApi"; //$NON-NLS-1$
|
||||
|
||||
private static final String APP_INDEXING_API_CLASS
|
||||
= "com.google.android.gms.appindexing.AppIndexApi";
|
||||
private static final String GOOGLE_API_CLIENT_CLASS
|
||||
= "com.google.android.gms.common.api.GoogleApiClient";
|
||||
private static final String GOOGLE_API_CLIENT_BUILDER_CLASS
|
||||
= "com.google.android.gms.common.api.GoogleApiClient.Builder";
|
||||
private static final String API_CLASS = "com.google.android.gms.appindexing.AppIndex";
|
||||
|
||||
public enum IssueType {
|
||||
SCHEME_MISSING(AppIndexingApiDetector.SCHEME_MISSING),
|
||||
HOST_MISSING(AppIndexingApiDetector.HOST_MISSING),
|
||||
DATA_MISSING(AppIndexingApiDetector.DATA_MISSING),
|
||||
URL_MISSING(AppIndexingApiDetector.URL_MISSING),
|
||||
NOT_BROWSABLE(AppIndexingApiDetector.NOT_BROWSABLE),
|
||||
ILLEGAL_NUMBER(AppIndexingApiDetector.ILLEGAL_NUMBER),
|
||||
EMPTY_FIELD("cannot be empty"),
|
||||
MISSING_SLASH("attribute should start with '/'"),
|
||||
UNKNOWN("unknown error type");
|
||||
|
||||
private final String message;
|
||||
|
||||
IssueType(String str) {
|
||||
this.message = str;
|
||||
}
|
||||
|
||||
public static IssueType parse(String str) {
|
||||
for (IssueType type : IssueType.values()) {
|
||||
if (str.contains(type.message)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Implements XmlScanner ----
|
||||
@Override
|
||||
@Nullable
|
||||
public Collection<String> getApplicableElements() {
|
||||
return Collections.singletonList(NODE_APPLICATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitElement(@NonNull XmlContext context, @NonNull Element application) {
|
||||
List<Element> activities = extractChildrenByName(application, NODE_ACTIVITY);
|
||||
boolean applicationHasActionView = false;
|
||||
for (Element activity : activities) {
|
||||
List<Element> intents = extractChildrenByName(activity, NODE_INTENT);
|
||||
boolean activityHasActionView = false;
|
||||
for (Element intent : intents) {
|
||||
boolean actionView = hasActionView(intent);
|
||||
if (actionView) {
|
||||
activityHasActionView = true;
|
||||
}
|
||||
visitIntent(context, intent);
|
||||
}
|
||||
if (activityHasActionView) {
|
||||
applicationHasActionView = true;
|
||||
if (activity.hasAttributeNS(ANDROID_URI, ATTR_EXPORTED)) {
|
||||
Attr exported = activity.getAttributeNodeNS(ANDROID_URI, ATTR_EXPORTED);
|
||||
if (!exported.getValue().equals("true")) {
|
||||
// Report error if the activity supporting action view is not exported.
|
||||
context.report(ISSUE_URL_ERROR, activity,
|
||||
context.getLocation(activity),
|
||||
"Activity supporting ACTION_VIEW is not exported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!applicationHasActionView && !context.getProject().isLibrary()) {
|
||||
// Report warning if there is no activity that supports action view.
|
||||
context.report(ISSUE_APP_INDEXING, application, context.getLocation(application),
|
||||
// This error message is more verbose than the other app indexing lint warnings, because it
|
||||
// shows up on a blank project, and we want to make it obvious by just looking at the error
|
||||
// message what this is
|
||||
"App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW " +
|
||||
"intent filter. See issue explanation for more details.");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> applicableSuperClasses() {
|
||||
return Collections.singletonList(CLASS_ACTIVITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
|
||||
if (declaration.getName() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In case linting the base class itself.
|
||||
if (!InheritanceUtil.isInheritor(declaration, true, CLASS_ACTIVITY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
declaration.accept(new MethodVisitor(context, declaration));
|
||||
}
|
||||
|
||||
static class MethodVisitor extends AbstractUastVisitor {
|
||||
private final JavaContext mContext;
|
||||
private final UClass mCls;
|
||||
|
||||
private final List<UCallExpression> mStartMethods;
|
||||
private final List<UCallExpression> mEndMethods;
|
||||
private final List<UCallExpression> mConnectMethods;
|
||||
private final List<UCallExpression> mDisconnectMethods;
|
||||
private boolean mHasAddAppIndexApi;
|
||||
|
||||
private MethodVisitor(JavaContext context, UClass cls) {
|
||||
mCls = cls;
|
||||
mContext = context;
|
||||
mStartMethods = Lists.newArrayListWithExpectedSize(2);
|
||||
mEndMethods = Lists.newArrayListWithExpectedSize(2);
|
||||
mConnectMethods = Lists.newArrayListWithExpectedSize(2);
|
||||
mDisconnectMethods = Lists.newArrayListWithExpectedSize(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visitClass(UClass aClass) {
|
||||
if (aClass.getPsi().equals(mCls.getPsi())) {
|
||||
return super.visitClass(aClass);
|
||||
} else {
|
||||
// Don't go into inner classes
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterVisitClass(UClass node) {
|
||||
report();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visitCallExpression(UCallExpression node) {
|
||||
if (UastExpressionUtils.isMethodCall(node)) {
|
||||
visitMethodCallExpression(node);
|
||||
}
|
||||
return super.visitCallExpression(node);
|
||||
}
|
||||
|
||||
private void visitMethodCallExpression(UCallExpression node) {
|
||||
String methodName = node.getMethodName();
|
||||
if (methodName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodName.equals(APP_INDEX_START)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
|
||||
mStartMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(APP_INDEX_END)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
|
||||
mEndMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(APP_INDEX_VIEW)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
|
||||
mStartMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(APP_INDEX_VIEW_END)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
|
||||
mEndMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(CLIENT_CONNECT)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) {
|
||||
mConnectMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(CLIENT_DISCONNECT)) {
|
||||
if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) {
|
||||
mDisconnectMethods.add(node);
|
||||
}
|
||||
}
|
||||
else if (methodName.equals(ADD_API)) {
|
||||
if (JavaEvaluator
|
||||
.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_BUILDER_CLASS)) {
|
||||
List<UExpression> args = node.getValueArguments();
|
||||
if (!args.isEmpty()) {
|
||||
PsiElement resolved = UastUtils.tryResolve(args.get(0));
|
||||
if (resolved instanceof PsiField &&
|
||||
JavaEvaluator.isMemberInClass((PsiField) resolved, API_CLASS)) {
|
||||
mHasAddAppIndexApi = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void report() {
|
||||
// finds the activity classes that need app activity annotation
|
||||
Set<String> activitiesToCheck = getActivitiesToCheck(mContext);
|
||||
|
||||
// app indexing API used but no support in manifest
|
||||
boolean hasIntent = activitiesToCheck.contains(mCls.getQualifiedName());
|
||||
if (!hasIntent) {
|
||||
for (UCallExpression call : mStartMethods) {
|
||||
mContext.report(ISSUE_APP_INDEXING_API, call,
|
||||
mContext.getUastNameLocation(call),
|
||||
"Missing support for Google App Indexing in the manifest");
|
||||
}
|
||||
for (UCallExpression call : mEndMethods) {
|
||||
mContext.report(ISSUE_APP_INDEXING_API, call,
|
||||
mContext.getUastNameLocation(call),
|
||||
"Missing support for Google App Indexing in the manifest");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// `AppIndex.AppIndexApi.start / end / view / viewEnd` should exist
|
||||
if (mStartMethods.isEmpty() && mEndMethods.isEmpty()) {
|
||||
mContext.reportUast(ISSUE_APP_INDEXING_API, mCls,
|
||||
mContext.getUastNameLocation(mCls),
|
||||
"Missing support for Google App Indexing API");
|
||||
return;
|
||||
}
|
||||
|
||||
for (UCallExpression startNode : mStartMethods) {
|
||||
List<UExpression> expressions = startNode.getValueArguments();
|
||||
if (expressions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
UExpression startClient = expressions.get(0);
|
||||
|
||||
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
|
||||
if (!mHasAddAppIndexApi) {
|
||||
String message = String.format(
|
||||
"GoogleApiClient `%1$s` has not added support for App Indexing API",
|
||||
startClient.asSourceString());
|
||||
mContext.report(ISSUE_APP_INDEXING_API, startClient,
|
||||
mContext.getUastLocation(startClient), message);
|
||||
}
|
||||
|
||||
// GoogleApiClient `connect` should exist
|
||||
if (!hasOperand(startClient, mConnectMethods)) {
|
||||
String message = String.format("GoogleApiClient `%1$s` is not connected",
|
||||
startClient.asSourceString());
|
||||
mContext.report(ISSUE_APP_INDEXING_API, startClient,
|
||||
mContext.getUastLocation(startClient), message);
|
||||
}
|
||||
|
||||
// `AppIndex.AppIndexApi.end` should pair with `AppIndex.AppIndexApi.start`
|
||||
if (!hasFirstArgument(startClient, mEndMethods)) {
|
||||
mContext.report(ISSUE_APP_INDEXING_API, startNode,
|
||||
mContext.getUastNameLocation(startNode),
|
||||
"Missing corresponding `AppIndex.AppIndexApi.end` method");
|
||||
}
|
||||
}
|
||||
|
||||
for (UCallExpression endNode : mEndMethods) {
|
||||
List<UExpression> expressions = endNode.getValueArguments();
|
||||
if (expressions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
UExpression endClient = expressions.get(0);
|
||||
|
||||
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
|
||||
if (!mHasAddAppIndexApi) {
|
||||
String message = String.format(
|
||||
"GoogleApiClient `%1$s` has not added support for App Indexing API",
|
||||
endClient.asSourceString());
|
||||
mContext.report(ISSUE_APP_INDEXING_API, endClient,
|
||||
mContext.getUastLocation(endClient), message);
|
||||
}
|
||||
|
||||
// GoogleApiClient `disconnect` should exist
|
||||
if (!hasOperand(endClient, mDisconnectMethods)) {
|
||||
String message = String.format("GoogleApiClient `%1$s`"
|
||||
+ " is not disconnected", endClient.asSourceString());
|
||||
mContext.report(ISSUE_APP_INDEXING_API, endClient,
|
||||
mContext.getUastLocation(endClient), message);
|
||||
}
|
||||
|
||||
// `AppIndex.AppIndexApi.start` should pair with `AppIndex.AppIndexApi.end`
|
||||
if (!hasFirstArgument(endClient, mStartMethods)) {
|
||||
mContext.report(ISSUE_APP_INDEXING_API, endNode,
|
||||
mContext.getUastNameLocation(endNode),
|
||||
"Missing corresponding `AppIndex.AppIndexApi.start` method");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets names of activities which needs app indexing. i.e. the activities have data tag in their
|
||||
* intent filters.
|
||||
* TODO: Cache the activities to speed up batch lint.
|
||||
*
|
||||
* @param context The context to check in.
|
||||
*/
|
||||
private static Set<String> getActivitiesToCheck(Context context) {
|
||||
Set<String> activitiesToCheck = Sets.newHashSet();
|
||||
List<File> manifestFiles = context.getProject().getManifestFiles();
|
||||
XmlParser xmlParser = context.getDriver().getClient().getXmlParser();
|
||||
if (xmlParser != null) {
|
||||
// TODO: Avoid visit all manifest files before enable this check by default.
|
||||
for (File manifest : manifestFiles) {
|
||||
XmlContext xmlContext =
|
||||
new XmlContext(context.getDriver(), context.getProject(),
|
||||
null, manifest, null, xmlParser);
|
||||
Document doc = xmlParser.parseXml(xmlContext);
|
||||
if (doc != null) {
|
||||
List<Element> children = LintUtils.getChildren(doc);
|
||||
for (Element child : children) {
|
||||
if (child.getNodeName().equals(NODE_MANIFEST)) {
|
||||
List<Element> apps = extractChildrenByName(child, NODE_APPLICATION);
|
||||
for (Element app : apps) {
|
||||
List<Element> acts = extractChildrenByName(app, NODE_ACTIVITY);
|
||||
for (Element act : acts) {
|
||||
List<Element> intents = extractChildrenByName(act, NODE_INTENT);
|
||||
for (Element intent : intents) {
|
||||
List<Element> data = extractChildrenByName(intent,
|
||||
NODE_DATA);
|
||||
if (!data.isEmpty() && act.hasAttributeNS(
|
||||
ANDROID_URI, ATTRIBUTE_NAME)) {
|
||||
Attr attr = act.getAttributeNodeNS(
|
||||
ANDROID_URI, ATTRIBUTE_NAME);
|
||||
String activityName = attr.getValue();
|
||||
int dotIndex = activityName.indexOf('.');
|
||||
if (dotIndex <= 0) {
|
||||
String pkg = context.getMainProject().getPackage();
|
||||
if (pkg != null) {
|
||||
if (dotIndex == 0) {
|
||||
activityName = pkg + activityName;
|
||||
}
|
||||
else {
|
||||
activityName = pkg + '.' + activityName;
|
||||
}
|
||||
}
|
||||
}
|
||||
activitiesToCheck.add(activityName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return activitiesToCheck;
|
||||
}
|
||||
|
||||
private static void visitIntent(@NonNull XmlContext context, @NonNull Element intent) {
|
||||
boolean actionView = hasActionView(intent);
|
||||
boolean browsable = isBrowsable(intent);
|
||||
boolean isHttp = false;
|
||||
boolean hasScheme = false;
|
||||
boolean hasHost = false;
|
||||
boolean hasPort = false;
|
||||
boolean hasPath = false;
|
||||
boolean hasMimeType = false;
|
||||
Element firstData = null;
|
||||
List<Element> children = extractChildrenByName(intent, NODE_DATA);
|
||||
for (Element data : children) {
|
||||
if (firstData == null) {
|
||||
firstData = data;
|
||||
}
|
||||
if (isHttpSchema(data)) {
|
||||
isHttp = true;
|
||||
}
|
||||
checkSingleData(context, data);
|
||||
|
||||
for (String name : PATH_ATTR_LIST) {
|
||||
if (data.hasAttributeNS(ANDROID_URI, name)) {
|
||||
hasPath = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
|
||||
hasScheme = true;
|
||||
}
|
||||
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) {
|
||||
hasHost = true;
|
||||
}
|
||||
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
|
||||
hasPort = true;
|
||||
}
|
||||
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) {
|
||||
hasMimeType = true;
|
||||
}
|
||||
}
|
||||
|
||||
// In data field, a URL is consisted by
|
||||
// <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
|
||||
// Each part of the URL should not have illegal character.
|
||||
if ((hasPath || hasHost || hasPort) && !hasScheme) {
|
||||
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
|
||||
SCHEME_MISSING);
|
||||
}
|
||||
|
||||
if ((hasPath || hasPort) && !hasHost) {
|
||||
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
|
||||
HOST_MISSING);
|
||||
}
|
||||
|
||||
if (actionView && browsable) {
|
||||
if (firstData == null) {
|
||||
// If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't
|
||||
// have data node, it may be a mistake and we will report error.
|
||||
context.report(ISSUE_URL_ERROR, intent, context.getLocation(intent),
|
||||
DATA_MISSING);
|
||||
} else if (!hasScheme && !hasMimeType) {
|
||||
// If this activity is an action view, is browsable, but has neither a
|
||||
// URL nor mimeType, it may be a mistake and we will report error.
|
||||
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
|
||||
URL_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
// If this activity is an ACTION_VIEW action, has a http URL but doesn't have
|
||||
// BROWSABLE, it may be a mistake and and we will report warning.
|
||||
if (actionView && isHttp && !browsable) {
|
||||
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
|
||||
NOT_BROWSABLE);
|
||||
}
|
||||
|
||||
if (actionView && !hasScheme) {
|
||||
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
|
||||
"Missing URL");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the intent filter supports action view.
|
||||
*
|
||||
* @param intent the intent filter
|
||||
* @return true if it does
|
||||
*/
|
||||
private static boolean hasActionView(@NonNull Element intent) {
|
||||
List<Element> children = extractChildrenByName(intent, NODE_ACTION);
|
||||
for (Element action : children) {
|
||||
if (action.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
|
||||
Attr attr = action.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
|
||||
if (attr.getValue().equals("android.intent.action.VIEW")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the intent filter is browsable.
|
||||
*
|
||||
* @param intent the intent filter
|
||||
* @return true if it does
|
||||
*/
|
||||
private static boolean isBrowsable(@NonNull Element intent) {
|
||||
List<Element> children = extractChildrenByName(intent, NODE_CATEGORY);
|
||||
for (Element e : children) {
|
||||
if (e.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
|
||||
Attr attr = e.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
|
||||
if (attr.getNodeValue().equals("android.intent.category.BROWSABLE")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the data node contains http schema
|
||||
*
|
||||
* @param data the data node
|
||||
* @return true if it does
|
||||
*/
|
||||
private static boolean isHttpSchema(@NonNull Element data) {
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
|
||||
String value = data.getAttributeNodeNS(ANDROID_URI, ATTR_SCHEME).getValue();
|
||||
if (value.equalsIgnoreCase("http") || value.equalsIgnoreCase("https")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void checkSingleData(@NonNull XmlContext context, @NonNull Element data) {
|
||||
// path, pathPrefix and pathPattern should starts with /.
|
||||
for (String name : PATH_ATTR_LIST) {
|
||||
if (data.hasAttributeNS(ANDROID_URI, name)) {
|
||||
Attr attr = data.getAttributeNodeNS(ANDROID_URI, name);
|
||||
String path = replaceUrlWithValue(context, attr.getValue());
|
||||
if (!path.startsWith("/") && !path.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
|
||||
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
|
||||
"android:" + name + " attribute should start with '/', but it is : "
|
||||
+ path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// port should be a legal number.
|
||||
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
|
||||
Attr attr = data.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_PORT);
|
||||
try {
|
||||
String port = replaceUrlWithValue(context, attr.getValue());
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
Integer.parseInt(port);
|
||||
} catch (NumberFormatException e) {
|
||||
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
|
||||
ILLEGAL_NUMBER);
|
||||
}
|
||||
}
|
||||
|
||||
// Each field should be non empty.
|
||||
NamedNodeMap attrs = data.getAttributes();
|
||||
for (int i = 0; i < attrs.getLength(); i++) {
|
||||
Node item = attrs.item(i);
|
||||
if (item.getNodeType() == Node.ATTRIBUTE_NODE) {
|
||||
Attr attr = (Attr) attrs.item(i);
|
||||
if (attr.getValue().isEmpty()) {
|
||||
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
|
||||
attr.getName() + " cannot be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String replaceUrlWithValue(@NonNull XmlContext context,
|
||||
@NonNull String str) {
|
||||
Project project = context.getProject();
|
||||
LintClient client = context.getClient();
|
||||
if (!client.supportsProjectResources()) {
|
||||
return str;
|
||||
}
|
||||
ResourceUrl style = ResourceUrl.parse(str);
|
||||
if (style == null || style.type != ResourceType.STRING || style.framework) {
|
||||
return str;
|
||||
}
|
||||
AbstractResourceRepository resources = client.getProjectResources(project, true);
|
||||
if (resources == null) {
|
||||
return str;
|
||||
}
|
||||
List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, style.name);
|
||||
if (items == null || items.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
ResourceValue resourceValue = items.get(0).getResourceValue(false);
|
||||
if (resourceValue == null) {
|
||||
return str;
|
||||
}
|
||||
return resourceValue.getValue() == null ? str : resourceValue.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* If a method with a certain argument exists in the list of methods.
|
||||
*
|
||||
* @param argument The first argument of the method.
|
||||
* @param list The methods list.
|
||||
* @return If such a method exists in the list.
|
||||
*/
|
||||
private static boolean hasFirstArgument(UExpression argument, List<UCallExpression> list) {
|
||||
for (UCallExpression call : list) {
|
||||
List<UExpression> expressions = call.getValueArguments();
|
||||
if (!expressions.isEmpty()) {
|
||||
UExpression argument2 = expressions.get(0);
|
||||
if (argument.asSourceString().equals(argument2.asSourceString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a method with a certain operand exists in the list of methods.
|
||||
*
|
||||
* @param operand The operand of the method.
|
||||
* @param list The methods list.
|
||||
* @return If such a method exists in the list.
|
||||
*/
|
||||
private static boolean hasOperand(UExpression operand, List<UCallExpression> list) {
|
||||
for (UCallExpression method : list) {
|
||||
UElement operand2 = method.getReceiver();
|
||||
if (operand2 != null && operand.asSourceString().equals(operand2.asSourceString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Element> extractChildrenByName(@NonNull Element node,
|
||||
@NonNull String name) {
|
||||
List<Element> result = Lists.newArrayList();
|
||||
List<Element> children = LintUtils.getChildren(node);
|
||||
for (Element child : children) {
|
||||
if (child.getNodeName().equals(name)) {
|
||||
result.add(child);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user