as31: Fix build for AS 2.4.0.3

(cherry picked from commit a79f7a7)
This commit is contained in:
Vyacheslav Gerasimov
2017-04-04 22:41:40 +03:00
committed by Nikolay Krasko
parent 8eb80a8f4a
commit 7d7fff52b8
8 changed files with 7570 additions and 0 deletions
@@ -0,0 +1,690 @@
/*
* 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;
}
}
@@ -0,0 +1,744 @@
/*
* 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;
}
}
@@ -0,0 +1,321 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_REF_PREFIX;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.RESOURCE_CLR_STYLEABLE;
import static com.android.SdkConstants.RESOURCE_CLZ_ARRAY;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
import static com.android.SdkConstants.TAG_ARRAY;
import static com.android.SdkConstants.TAG_INTEGER_ARRAY;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_PLURALS;
import static com.android.SdkConstants.TAG_RESOURCES;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.tools.klint.detector.api.LintUtils.getBaseName;
import static com.android.utils.SdkUtils.getResourceFieldName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.MavenCoordinates;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.resources.ResourceUrl;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
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.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.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
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 org.jetbrains.uast.UElement;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Check which looks for access of private resources.
*/
public class PrivateResourceDetector extends ResourceXmlDetector implements
Detector.UastScanner {
/** Attribute for overriding a resource */
private static final String ATTR_OVERRIDE = "override";
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
PrivateResourceDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"PrivateResource", //$NON-NLS-1$
"Using private resources",
"Private resources should not be referenced; the may not be present everywhere, and " +
"even where they are they may disappear without notice.\n" +
"\n" +
"To fix this, copy the resource into your own project instead.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new detector */
public PrivateResourceDetector() {
}
// ---- Implements UastScanner ----
@Override
public boolean appliesToResourceRefs() {
return true;
}
@Override
public void visitResourceReference(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UElement node, @NonNull ResourceType resourceType, @NonNull String name,
boolean isFramework) {
if (context.getProject().isGradleProject() && !isFramework) {
Project project = context.getProject();
if (project.getGradleProjectModel() != null && project.getCurrentVariant() != null) {
if (isPrivate(context, resourceType, name)) {
String message = createUsageErrorMessage(context, resourceType, name);
context.report(ISSUE, node, context.getUastLocation(node), message);
}
}
}
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
/** Check resource references: accessing a private resource from an upstream library? */
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getNodeValue();
if (context.getProject().isGradleProject()) {
ResourceUrl url = ResourceUrl.parse(value);
if (isPrivate(context, url)) {
String message = createUsageErrorMessage(context, url.type, url.name);
context.report(ISSUE, attribute, context.getValueLocation(attribute), message);
}
}
}
/** Check resource definitions: overriding a private resource from an upstream library? */
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_STYLE,
TAG_RESOURCES,
TAG_ARRAY,
TAG_STRING_ARRAY,
TAG_INTEGER_ARRAY,
TAG_PLURALS
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (TAG_RESOURCES.equals(element.getTagName())) {
for (Element item : LintUtils.getChildren(element)) {
Attr nameAttribute = item.getAttributeNode(ATTR_NAME);
if (nameAttribute != null) {
String name = getResourceFieldName(nameAttribute.getValue());
String type = item.getTagName();
if (type.equals(TAG_ITEM)) {
type = item.getAttribute(ATTR_TYPE);
if (type == null || type.isEmpty()) {
type = RESOURCE_CLZ_ID;
}
} else if (type.equals("declare-styleable")) { //$NON-NLS-1$
type = RESOURCE_CLR_STYLEABLE;
} else if (type.contains("array")) { //$NON-NLS-1$
// <string-array> etc
type = RESOURCE_CLZ_ARRAY;
}
ResourceType t = ResourceType.getEnum(type);
if (t != null && isPrivate(context, t, name) &&
!VALUE_TRUE.equals(item.getAttributeNS(TOOLS_URI, ATTR_OVERRIDE))) {
String message = createOverrideErrorMessage(context, t, name);
Location location = context.getValueLocation(nameAttribute);
context.report(ISSUE, nameAttribute, location, message);
}
}
}
} else {
assert TAG_STYLE.equals(element.getTagName())
|| TAG_ARRAY.equals(element.getTagName())
|| TAG_PLURALS.equals(element.getTagName())
|| TAG_INTEGER_ARRAY.equals(element.getTagName())
|| TAG_STRING_ARRAY.equals(element.getTagName());
for (Element item : LintUtils.getChildren(element)) {
checkChildRefs(context, item);
}
}
}
private static boolean isPrivate(Context context, ResourceType type, String name) {
if (type == ResourceType.ID) {
// No need to complain about "overriding" id's. There's no harm
// in doing so. (This avoids warning about cases like for example
// appcompat's (private) @id/title resource, which would otherwise
// flag any attempt to create a resource named title in the user's
// project.
return false;
}
if (context.getProject().isGradleProject()) {
ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility();
return lookup.isPrivate(type, name);
}
return false;
}
private static boolean isPrivate(@NonNull Context context, @Nullable ResourceUrl url) {
return url != null && !url.framework && isPrivate(context, url.type, url.name);
}
private static void checkChildRefs(@NonNull XmlContext context, Element item) {
// Look for ?attr/ and @dimen/foo etc references in the item children
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
int index = text.indexOf(ATTR_REF_PREFIX);
if (index != -1) {
String name = text.substring(index + ATTR_REF_PREFIX.length()).trim();
if (isPrivate(context, ResourceType.ATTR, name)) {
String message = createUsageErrorMessage(context, ResourceType.ATTR, name);
context.report(ISSUE, item, context.getLocation(child), message);
}
} else {
for (int j = 0, m = text.length(); j < m; j++) {
char c = text.charAt(j);
if (c == '@') {
ResourceUrl url = ResourceUrl.parse(text.trim());
if (isPrivate(context, url)) {
String message = createUsageErrorMessage(context, url.type,
url.name);
context.report(ISSUE, item, context.getLocation(child), message);
}
break;
} else if (!Character.isWhitespace(c)) {
break;
}
}
}
}
}
}
@Override
public void beforeCheckFile(@NonNull Context context) {
File file = context.file;
boolean isXmlFile = LintUtils.isXmlFile(file);
if (!isXmlFile && !LintUtils.isBitmapFile(file)) {
return;
}
String parentName = file.getParentFile().getName();
int dash = parentName.indexOf('-');
if (dash != -1 || FD_RES_VALUES.equals(parentName)) {
return;
}
ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName);
if (folderType == null) {
return;
}
List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
if (types.isEmpty()) {
return;
}
ResourceType type = types.get(0);
String resourceName = getResourceFieldName(getBaseName(file.getName()));
if (isPrivate(context, type, resourceName)) {
String message = createOverrideErrorMessage(context, type, resourceName);
Location location = Location.create(file);
context.report(ISSUE, location, message);
}
}
private static String createOverrideErrorMessage(@NonNull Context context,
@NonNull ResourceType type, @NonNull String name) {
String libraryName = getLibraryName(context, type, name);
return String.format("Overriding `@%1$s/%2$s` which is marked as private in %3$s. If "
+ "deliberate, use tools:override=\"true\", otherwise pick a "
+ "different name.", type, name, libraryName);
}
private static String createUsageErrorMessage(@NonNull Context context,
@NonNull ResourceType type, @NonNull String name) {
String libraryName = getLibraryName(context, type, name);
return String.format("The resource `@%1$s/%2$s` is marked as private in %3$s", type,
name, libraryName);
}
/** Pick a suitable name to describe the library defining the private resource */
@Nullable
private static String getLibraryName(@NonNull Context context, @NonNull ResourceType type,
@NonNull String name) {
ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility();
AndroidLibrary library = lookup.getPrivateIn(type, name);
if (library != null) {
String libraryName = library.getProject();
if (libraryName != null) {
return libraryName;
}
MavenCoordinates coordinates = library.getResolvedCoordinates();
if (coordinates != null) {
return coordinates.getGroupId() + ':' + coordinates.getArtifactId();
}
}
return "the library";
}
}
@@ -0,0 +1,621 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.FD_RES_LAYOUT;
import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.REQUEST_FOCUS;
import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAG_DATA;
import static com.android.SdkConstants.TAG_IMPORT;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_LAYOUT;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.TAG_VARIABLE;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.SdkConstants.VIEW_MERGE;
import static com.android.resources.ResourceFolderType.LAYOUT;
import static com.android.resources.ResourceFolderType.VALUES;
import static com.android.tools.klint.detector.api.LintUtils.getLayoutName;
import static com.android.tools.klint.detector.api.LintUtils.isNullLiteral;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
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.JavaPsiScanner;
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.LayoutDetector;
import com.android.tools.klint.detector.api.ResourceEvaluator;
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.Maps;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastLiteralUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Element;
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.Map;
import java.util.Set;
/**
* Ensures that layout width and height attributes are specified
*/
public class RequiredAttributeDetector extends LayoutDetector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"RequiredSize", //$NON-NLS-1$
"Missing `layout_width` or `layout_height` attributes",
"All views must specify an explicit `layout_width` and `layout_height` attribute. " +
"There is a runtime check for this, so if you fail to specify a size, an exception " +
"is thrown at runtime.\n" +
"\n" +
"It's possible to specify these widths via styles as well. GridLayout, as a special " +
"case, does not require you to specify a size.",
Category.CORRECTNESS,
4,
Severity.ERROR,
new Implementation(
RequiredAttributeDetector.class,
EnumSet.of(Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES)));
public static final String PERCENT_RELATIVE_LAYOUT
= "android.support.percent.PercentRelativeLayout";
public static final String ATTR_LAYOUT_WIDTH_PERCENT = "layout_widthPercent";
public static final String ATTR_LAYOUT_HEIGHT_PERCENT = "layout_heightPercent";
/** Map from each style name to parent style */
@Nullable private Map<String, String> mStyleParents;
/** Set of style names where the style sets the layout width */
@Nullable private Set<String> mWidthStyles;
/** Set of style names where the style sets the layout height */
@Nullable private Set<String> mHeightStyles;
/** Set of layout names for layouts that are included by an {@code <include>} tag
* where the width is set on the include */
@Nullable private Set<String> mIncludedWidths;
/** Set of layout names for layouts that are included by an {@code <include>} tag
* where the height is set on the include */
@Nullable private Set<String> mIncludedHeights;
/** Set of layout names for layouts that are included by an {@code <include>} tag
* where the width is <b>not</b> set on the include */
@Nullable private Set<String> mNotIncludedWidths;
/** Set of layout names for layouts that are included by an {@code <include>} tag
* where the height is <b>not</b> set on the include */
@Nullable private Set<String> mNotIncludedHeights;
/** Whether the width was set in a theme definition */
private boolean mSetWidthInTheme;
/** Whether the height was set in a theme definition */
private boolean mSetHeightInTheme;
/** Constructs a new {@link RequiredAttributeDetector} */
public RequiredAttributeDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == LAYOUT || folderType == VALUES;
}
@Override
public void afterCheckProject(@NonNull Context context) {
// Process checks in two phases:
// Phase 1: Gather styles and includes (styles are encountered after the layouts
// so we can't do it in a single phase, and includes can be affected by includes from
// layouts we haven't seen yet)
// Phase 2: Process layouts, using gathered style and include data, and mark layouts
// not known.
//
if (context.getPhase() == 1) {
checkSizeSetInTheme();
context.requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
}
}
private boolean isWidthStyle(String style) {
return isSizeStyle(style, mWidthStyles);
}
private boolean isHeightStyle(String style) {
return isSizeStyle(style, mHeightStyles);
}
private boolean isSizeStyle(String style, Set<String> sizeStyles) {
if (isFrameworkSizeStyle(style)) {
return true;
}
if (sizeStyles == null) {
return false;
}
return isSizeStyle(stripStylePrefix(style), sizeStyles, 0);
}
private static boolean isFrameworkSizeStyle(String style) {
// The styles Widget.TextView.ListSeparator (and several theme variations, such as
// Widget.Holo.TextView.ListSeparator, Widget.Holo.Light.TextView.ListSeparator, etc)
// define layout_width and layout_height.
// These are exposed through the listSeparatorTextViewStyle style.
if (style.equals("?android:attr/listSeparatorTextViewStyle") //$NON-NLS-1$
|| style.equals("?android/listSeparatorTextViewStyle")) { //$NON-NLS-1$
return true;
}
// It's also set on Widget.QuickContactBadge and Widget.QuickContactBadgeSmall
// These are exposed via a handful of attributes with a common prefix
if (style.startsWith("?android:attr/quickContactBadgeStyle")) { //$NON-NLS-1$
return true;
}
// Finally, the styles are set on MediaButton and Widget.Holo.Tab (and
// Widget.Holo.Light.Tab) but these are not exposed via attributes.
return false;
}
private boolean isSizeStyle(
@NonNull String style,
@NonNull Set<String> sizeStyles, int depth) {
if (depth == 30) {
// Cycle between local and framework attribute style missed
// by the fact that we're stripping the distinction between framework
// and local styles here
return false;
}
assert !style.startsWith(STYLE_RESOURCE_PREFIX)
&& !style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
if (sizeStyles.contains(style)) {
return true;
}
if (mStyleParents != null) {
String parentStyle = mStyleParents.get(style);
if (parentStyle != null) {
parentStyle = stripStylePrefix(parentStyle);
if (isSizeStyle(parentStyle, sizeStyles, depth + 1)) {
return true;
}
}
}
int index = style.lastIndexOf('.');
if (index > 0) {
return isSizeStyle(style.substring(0, index), sizeStyles, depth + 1);
}
return false;
}
private void checkSizeSetInTheme() {
// Look through the styles and determine whether each style is a theme
if (mStyleParents == null) {
return;
}
Map<String, Boolean> isTheme = Maps.newHashMap();
for (String style : mStyleParents.keySet()) {
if (isTheme(stripStylePrefix(style), isTheme, 0)) {
mSetWidthInTheme = true;
mSetHeightInTheme = true;
break;
}
}
}
private boolean isTheme(String style, Map<String, Boolean> isTheme, int depth) {
if (depth == 30) {
// Cycle between local and framework attribute style missed
// by the fact that we're stripping the distinction between framework
// and local styles here
return false;
}
assert !style.startsWith(STYLE_RESOURCE_PREFIX)
&& !style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
Boolean known = isTheme.get(style);
if (known != null) {
return known;
}
if (style.contains("Theme")) { //$NON-NLS-1$
isTheme.put(style, true);
return true;
}
if (mStyleParents != null) {
String parentStyle = mStyleParents.get(style);
if (parentStyle != null) {
parentStyle = stripStylePrefix(parentStyle);
if (isTheme(parentStyle, isTheme, depth + 1)) {
isTheme.put(style, true);
return true;
}
}
}
int index = style.lastIndexOf('.');
if (index > 0) {
String parentStyle = style.substring(0, index);
boolean result = isTheme(parentStyle, isTheme, depth + 1);
isTheme.put(style, result);
return result;
}
return false;
}
@VisibleForTesting
static boolean hasLayoutVariations(File file) {
File parent = file.getParentFile();
if (parent == null) {
return false;
}
File res = parent.getParentFile();
if (res == null) {
return false;
}
String name = file.getName();
File[] folders = res.listFiles();
if (folders == null) {
return false;
}
for (File folder : folders) {
if (!folder.getName().startsWith(FD_RES_LAYOUT)) {
continue;
}
if (folder.equals(parent)) {
continue;
}
File other = new File(folder, name);
if (other.exists()) {
return true;
}
}
return false;
}
private static String stripStylePrefix(@NonNull String style) {
if (style.startsWith(STYLE_RESOURCE_PREFIX)) {
style = style.substring(STYLE_RESOURCE_PREFIX.length());
} else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length());
}
return style;
}
private static boolean isRootElement(@NonNull Node node) {
return node == node.getOwnerDocument().getDocumentElement();
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return ALL;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
ResourceFolderType folderType = context.getResourceFolderType();
int phase = context.getPhase();
if (phase == 1 && folderType == VALUES) {
String tag = element.getTagName();
if (TAG_STYLE.equals(tag)) {
String parent = element.getAttribute(ATTR_PARENT);
if (parent != null && !parent.isEmpty()) {
String name = element.getAttribute(ATTR_NAME);
if (name != null && !name.isEmpty()) {
if (mStyleParents == null) {
mStyleParents = Maps.newHashMap();
}
mStyleParents.put(name, parent);
}
}
} else if (TAG_ITEM.equals(tag)
&& TAG_STYLE.equals(element.getParentNode().getNodeName())) {
String name = element.getAttribute(ATTR_NAME);
if (name.endsWith(ATTR_LAYOUT_WIDTH) &&
name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_WIDTH)) {
if (mWidthStyles == null) {
mWidthStyles = Sets.newHashSet();
}
String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME);
mWidthStyles.add(styleName);
}
if (name.endsWith(ATTR_LAYOUT_HEIGHT) &&
name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_HEIGHT)) {
if (mHeightStyles == null) {
mHeightStyles = Sets.newHashSet();
}
String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME);
mHeightStyles.add(styleName);
}
}
} else if (folderType == LAYOUT) {
if (phase == 1) {
// Gather includes
if (element.getTagName().equals(VIEW_INCLUDE)) {
String layout = element.getAttribute(ATTR_LAYOUT);
if (layout != null && !layout.isEmpty()) {
recordIncludeWidth(layout,
element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH));
recordIncludeHeight(layout,
element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT));
}
}
} else {
assert phase == 2; // Check everything using style data and include data
boolean hasWidth = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
boolean hasHeight = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
if (mSetWidthInTheme) {
hasWidth = true;
}
if (mSetHeightInTheme) {
hasHeight = true;
}
if (hasWidth && hasHeight) {
return;
}
String tag = element.getTagName();
if (VIEW_MERGE.equals(tag)
|| VIEW_INCLUDE.equals(tag)
|| REQUEST_FOCUS.equals(tag)) {
return;
}
// Data binding: these tags shouldn't specify width/height
if (tag.equals(TAG_LAYOUT)
|| tag.equals(TAG_VARIABLE)
|| tag.equals(TAG_DATA)
|| tag.equals(TAG_IMPORT)) {
return;
}
String parentTag = element.getParentNode() != null
? element.getParentNode().getNodeName() : "";
if (TABLE_LAYOUT.equals(parentTag)
|| TABLE_ROW.equals(parentTag)
|| GRID_LAYOUT.equals(parentTag)
|| FQCN_GRID_LAYOUT_V7.equals(parentTag)) {
return;
}
// PercentRelativeLayout or PercentFrameLayout?
boolean isPercent = parentTag.startsWith("android.support.percent.Percent");
if (isPercent) {
hasWidth |= element.hasAttributeNS(AUTO_URI, ATTR_LAYOUT_WIDTH_PERCENT);
hasHeight |= element.hasAttributeNS(AUTO_URI, ATTR_LAYOUT_HEIGHT_PERCENT);
if (hasWidth && hasHeight) {
return;
}
}
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
boolean certain = true;
boolean isRoot = isRootElement(element);
if (isRoot || isRootElement(element.getParentNode())
&& VIEW_MERGE.equals(parentTag)) {
String name = LAYOUT_RESOURCE_PREFIX + getLayoutName(context.file);
if (!hasWidth && mIncludedWidths != null) {
hasWidth = mIncludedWidths.contains(name);
// If the layout is *also* included in a context where the width
// was not set, we're not certain; it's possible that
if (mNotIncludedWidths != null && mNotIncludedWidths.contains(name)) {
hasWidth = false;
// If we only have a single layout we know that this layout isn't
// always included with layout_width or layout_height set, but
// if there are multiple layouts, it's possible that at runtime
// we only load the size-less layout by the tag which includes
// the size
certain = !hasLayoutVariations(context.file);
}
}
if (!hasHeight && mIncludedHeights != null) {
hasHeight = mIncludedHeights.contains(name);
if (mNotIncludedHeights != null && mNotIncludedHeights.contains(name)) {
hasHeight = false;
certain = !hasLayoutVariations(context.file);
}
}
if (hasWidth && hasHeight) {
return;
}
}
if (!hasWidth || !hasHeight) {
String style = element.getAttribute(ATTR_STYLE);
if (style != null && !style.isEmpty()) {
if (!hasWidth) {
hasWidth = isWidthStyle(style);
}
if (!hasHeight) {
hasHeight = isHeightStyle(style);
}
}
if (hasWidth && hasHeight) {
return;
}
}
String message;
if (!(hasWidth || hasHeight)) {
if (certain) {
message = "The required `layout_width` and `layout_height` attributes " +
"are missing";
} else {
message = "The required `layout_width` and `layout_height` attributes " +
"*may* be missing";
}
} else {
String attribute = hasWidth ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
if (certain) {
message = String.format("The required `%1$s` attribute is missing",
attribute);
} else {
message = String.format("The required `%1$s` attribute *may* be missing",
attribute);
}
}
if (isPercent) {
String escapedLayoutWidth = '`' + ATTR_LAYOUT_WIDTH + '`';
String escapedLayoutHeight = '`' + ATTR_LAYOUT_HEIGHT + '`';
String escapedLayoutWidthPercent = '`' + ATTR_LAYOUT_WIDTH_PERCENT + '`';
String escapedLayoutHeightPercent = '`' + ATTR_LAYOUT_HEIGHT_PERCENT + '`';
message = message.replace(escapedLayoutWidth, escapedLayoutWidth + " or "
+ escapedLayoutWidthPercent).replace(escapedLayoutHeight,
escapedLayoutHeight + " or " + escapedLayoutHeightPercent);
}
context.report(ISSUE, element, context.getLocation(element),
message);
}
}
}
private void recordIncludeWidth(String layout, boolean providesWidth) {
if (providesWidth) {
if (mIncludedWidths == null) {
mIncludedWidths = Sets.newHashSet();
}
mIncludedWidths.add(layout);
} else {
if (mNotIncludedWidths == null) {
mNotIncludedWidths = Sets.newHashSet();
}
mNotIncludedWidths.add(layout);
}
}
private void recordIncludeHeight(String layout, boolean providesHeight) {
if (providesHeight) {
if (mIncludedHeights == null) {
mIncludedHeights = Sets.newHashSet();
}
mIncludedHeights.add(layout);
} else {
if (mNotIncludedHeights == null) {
mNotIncludedHeights = Sets.newHashSet();
}
mNotIncludedHeights.add(layout);
}
}
// ---- Implements UastScanner ----
@Override
@Nullable
public List<String> getApplicableMethodNames() {
return Collections.singletonList("inflate"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
// Handle
// View#inflate(Context context, int resource, ViewGroup root)
// LayoutInflater#inflate(int resource, ViewGroup root)
// LayoutInflater#inflate(int resource, ViewGroup root, boolean attachToRoot)
List<UExpression> args = call.getValueArguments();
String layout = null;
int index = 0;
ResourceEvaluator evaluator = new ResourceEvaluator(context);
for (UExpression expression : args) {
ResourceUrl url = evaluator.getResource(expression);
if (url != null && url.type == ResourceType.LAYOUT) {
layout = url.toString();
break;
}
index++;
}
if (layout == null) {
// Flow analysis didn't succeed
return;
}
// In all the applicable signatures, the view root argument is immediately after
// the layout resource id.
int viewRootPos = index + 1;
if (viewRootPos < args.size()) {
UExpression viewRoot = args.get(viewRootPos);
if (UastLiteralUtils.isNullLiteral(viewRoot)) {
// Yep, this one inflates the given view with a null parent:
// Tag it as such. For now just use the include data structure since
// it has the same net effect
recordIncludeWidth(layout, true);
recordIncludeHeight(layout, true);
}
}
}
}
@@ -0,0 +1,308 @@
/*
* Copyright (C) 2011 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.*;
import com.android.utils.XmlUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.*;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiType;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.*;
import java.io.File;
import java.util.*;
import static com.android.SdkConstants.*;
/** Detector for finding inconsistent usage of views and casts
* <p>
* TODO: Check findFragmentById
* <pre>
* ((ItemListFragment) getSupportFragmentManager()
* .findFragmentById(R.id.item_list))
* .setActivateOnItemClick(true);
* </pre>
* Here we should check the {@code <fragment>} tag pointed to by the id, and
* check its name or class attributes to make sure the cast is compatible with
* the named fragment class!
*/
public class ViewTypeDetector extends ResourceXmlDetector implements Detector.UastScanner {
/** Mismatched view types */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"WrongViewCast", //$NON-NLS-1$
"Mismatched view type",
"Keeps track of the view types associated with ids and if it finds a usage of " +
"the id in the Java code it ensures that it is treated as the same type.",
Category.CORRECTNESS,
9,
Severity.FATAL,
new Implementation(
ViewTypeDetector.class,
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES),
Scope.JAVA_FILE_SCOPE));
/** Flag used to do no work if we're running in incremental mode in a .java file without
* a client supporting project resources */
private Boolean mIgnore = null;
private final Map<String, Object> mIdToViewTag = new HashMap<String, Object>(50);
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ID);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String view = attribute.getOwnerElement().getTagName();
String value = attribute.getValue();
String id = null;
if (value.startsWith(ID_PREFIX)) {
id = value.substring(ID_PREFIX.length());
} else if (value.startsWith(NEW_ID_PREFIX)) {
id = value.substring(NEW_ID_PREFIX.length());
} // else: could be @android id
if (id != null) {
if (view.equals(VIEW_TAG)) {
view = attribute.getOwnerElement().getAttribute(ATTR_CLASS);
}
Object existing = mIdToViewTag.get(id);
if (existing == null) {
mIdToViewTag.put(id, view);
} else if (existing instanceof String) {
String existingString = (String) existing;
if (!existingString.equals(view)) {
// Convert to list
List<String> list = new ArrayList<String>(2);
list.add((String) existing);
list.add(view);
mIdToViewTag.put(id, list);
}
} else if (existing instanceof List<?>) {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) existing;
if (!list.contains(view)) {
list.add(view);
}
}
}
}
// ---- Implements Detector.JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
LintClient client = context.getClient();
if (mIgnore == Boolean.TRUE) {
return;
} else if (mIgnore == null) {
mIgnore = !context.getScope().contains(Scope.ALL_RESOURCE_FILES) &&
!client.supportsProjectResources();
if (mIgnore) {
return;
}
}
assert method.getName().equals("findViewById");
UElement node = LintUtils.skipParentheses(call);
while (node != null && node.getUastParent() instanceof UParenthesizedExpression) {
node = node.getUastParent();
}
if (node.getUastParent() instanceof UBinaryExpressionWithType) {
UBinaryExpressionWithType cast = (UBinaryExpressionWithType) node.getUastParent();
PsiType type = cast.getType();
String castType = null;
if (type instanceof PsiClassType) {
castType = type.getCanonicalText();
}
if (castType == null) {
return;
}
List<UExpression> args = call.getValueArguments();
if (args.size() == 1) {
UExpression first = args.get(0);
ResourceUrl resourceUrl = ResourceEvaluator.getResource(context, first);
if (resourceUrl != null && resourceUrl.type == ResourceType.ID &&
!resourceUrl.framework) {
String id = resourceUrl.name;
if (client.supportsProjectResources()) {
AbstractResourceRepository resources = client
.getProjectResources(context.getMainProject(), true);
if (resources == null) {
return;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.ID,
id);
if (items != null && !items.isEmpty()) {
Set<String> compatible = Sets.newHashSet();
for (ResourceItem item : items) {
Collection<String> tags = getViewTags(context, item);
if (tags != null) {
compatible.addAll(tags);
}
}
if (!compatible.isEmpty()) {
ArrayList<String> layoutTypes = Lists.newArrayList(compatible);
checkCompatible(context, castType, null, layoutTypes, cast);
}
}
} else {
Object types = mIdToViewTag.get(id);
if (types instanceof String) {
String layoutType = (String) types;
checkCompatible(context, castType, layoutType, null, cast);
} else if (types instanceof List<?>) {
@SuppressWarnings("unchecked")
List<String> layoutTypes = (List<String>) types;
checkCompatible(context, castType, null, layoutTypes, cast);
}
}
}
}
}
}
@Nullable
protected Collection<String> getViewTags(
@NonNull Context context,
@NonNull ResourceItem item) {
// Check view tag in this file. Can I do it cheaply? Try with
// an XML pull parser. Or DOM if we have multiple resources looked
// up?
ResourceFile source = item.getSource();
if (source != null) {
File file = source.getFile();
Multimap<String,String> map = getIdToTagsIn(context, file);
if (map != null) {
return map.get(item.getName());
}
}
return null;
}
private Map<File, Multimap<String, String>> mFileIdMap;
@Nullable
private Multimap<String, String> getIdToTagsIn(@NonNull Context context, @NonNull File file) {
if (!file.getPath().endsWith(DOT_XML)) {
return null;
}
if (mFileIdMap == null) {
mFileIdMap = Maps.newHashMap();
}
Multimap<String, String> map = mFileIdMap.get(file);
if (map == null) {
map = ArrayListMultimap.create();
mFileIdMap.put(file, map);
String xml = context.getClient().readFile(file);
// TODO: Use pull parser instead for better performance!
Document document = XmlUtils.parseDocumentSilently(xml, true);
if (document != null && document.getDocumentElement() != null) {
addViewTags(map, document.getDocumentElement());
}
}
return map;
}
private static void addViewTags(Multimap<String, String> map, Element element) {
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty()) {
id = LintUtils.stripIdPrefix(id);
if (!map.containsEntry(id, element.getTagName())) {
map.put(id, element.getTagName());
}
}
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addViewTags(map, (Element) child);
}
}
}
/** Check if the view and cast type are compatible */
private static void checkCompatible(JavaContext context, String castType, String layoutType,
List<String> layoutTypes, UBinaryExpressionWithType node) {
assert layoutType == null || layoutTypes == null; // Should only specify one or the other
boolean compatible = true;
if (layoutType != null) {
if (!layoutType.equals(castType)
&& !context.getSdkInfo().isSubViewOf(castType, layoutType)) {
compatible = false;
}
} else {
compatible = false;
assert layoutTypes != null;
for (String type : layoutTypes) {
if (type.equals(castType)
|| context.getSdkInfo().isSubViewOf(castType, type)) {
compatible = true;
break;
}
}
}
if (!compatible) {
if (layoutType == null) {
layoutType = Joiner.on("|").join(layoutTypes);
}
String message = String.format(
"Unexpected cast to `%1$s`: layout tag was `%2$s`",
castType.substring(castType.lastIndexOf('.') + 1), layoutType);
context.report(ISSUE, node, context.getUastLocation(node), message);
}
}
}