as31: Fix build for AS 2.4.0.3
(cherry picked from commit a79f7a7)
This commit is contained in:
committed by
Nikolay Krasko
parent
8eb80a8f4a
commit
7d7fff52b8
File diff suppressed because it is too large
Load Diff
+690
@@ -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;
|
||||
}
|
||||
}
|
||||
+744
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+321
@@ -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";
|
||||
}
|
||||
}
|
||||
+621
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1494
File diff suppressed because it is too large
Load Diff
+308
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user