blob: 6987d30fffebda537715a3f8343ecb012dc6ee18 [file] [log] [blame]
/*
* 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.lint.checks;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.SdkConstants.FQCN_SUPPRESS_LINT;
import static com.android.SdkConstants.INT_DEF_ANNOTATION;
import static com.android.SdkConstants.LONG_DEF_ANNOTATION;
import static com.android.SdkConstants.STRING_DEF_ANNOTATION;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_DOUBLE;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_FLOAT;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_INT;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_LONG;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_SHORT;
import static com.android.tools.lint.client.api.JavaEvaluatorKt.TYPE_STRING;
import static com.android.tools.lint.detector.api.Lint.getAutoBoxedType;
import static com.android.tools.lint.detector.api.ResourceEvaluator.COLOR_INT_ANNOTATION;
import static com.android.tools.lint.detector.api.ResourceEvaluator.DIMENSION_ANNOTATION;
import static com.android.tools.lint.detector.api.ResourceEvaluator.PX_ANNOTATION;
import static com.android.tools.lint.detector.api.ResourceEvaluator.RES_SUFFIX;
import static com.android.tools.lint.detector.api.UastLintUtils.getAnnotationBooleanValue;
import static com.android.tools.lint.detector.api.UastLintUtils.getAnnotationStringValue;
import static com.android.tools.lint.detector.api.UastLintUtils.getAnnotationStringValues;
import static com.android.tools.lint.detector.api.UastLintUtils.getDoubleAttribute;
import static com.android.tools.lint.detector.api.UastLintUtils.getLongAttribute;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.support.AndroidxName;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.client.api.UElementHandler;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.ExternalReferenceExpression;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintFix;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.android.tools.lint.detector.api.UastLintUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiArrayType;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCompiledElement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.search.GlobalSearchScope;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import kotlin.collections.CollectionsKt;
import org.jetbrains.uast.UAnnotation;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UDeclaration;
import org.jetbrains.uast.UDeclarationsExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIdentifier;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.ULocalVariable;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UNamedExpression;
import org.jetbrains.uast.UParameter;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UReferenceExpression;
import org.jetbrains.uast.USwitchClauseExpression;
import org.jetbrains.uast.USwitchExpression;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UastFacade;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.java.JavaUAnnotation;
import org.jetbrains.uast.java.JavaUTypeCastExpression;
import org.jetbrains.uast.kotlin.KotlinUSwitchExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/** Checks annotations to make sure they are valid */
public class AnnotationDetector extends Detector implements SourceCodeScanner {
public static final String GMS_HIDE_ANNOTATION = "com.google.android.gms.common.internal.Hide";
public static final AndroidxName CHECK_RESULT_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "CheckResult");
public static final AndroidxName INT_RANGE_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "IntRange");
public static final AndroidxName FLOAT_RANGE_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "FloatRange");
public static final AndroidxName SIZE_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "Size");
public static final AndroidxName PERMISSION_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX.oldName(), "RequiresPermission");
public static final AndroidxName UI_THREAD_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "UiThread");
public static final AndroidxName MAIN_THREAD_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "MainThread");
public static final AndroidxName WORKER_THREAD_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "WorkerThread");
public static final AndroidxName BINDER_THREAD_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "BinderThread");
public static final AndroidxName ANY_THREAD_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "AnyThread");
public static final AndroidxName RESTRICT_TO_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "RestrictTo");
public static final AndroidxName VISIBLE_FOR_TESTING_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "VisibleForTesting");
public static final AndroidxName PERMISSION_ANNOTATION_READ =
AndroidxName.of(PERMISSION_ANNOTATION, "Read");
public static final AndroidxName PERMISSION_ANNOTATION_WRITE =
AndroidxName.of(PERMISSION_ANNOTATION, "Write");
// TODO: Add analysis to enforce this annotation:
public static final AndroidxName HALF_FLOAT_ANNOTATION =
AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "HalfFloat");
public static final String THREAD_SUFFIX = "Thread";
public static final String ATTR_SUGGEST = "suggest";
public static final String ATTR_TO = "to";
public static final String ATTR_FROM = "from";
public static final String ATTR_FROM_INCLUSIVE = "fromInclusive";
public static final String ATTR_TO_INCLUSIVE = "toInclusive";
public static final String ATTR_MULTIPLE = "multiple";
public static final String ATTR_MIN = "min";
public static final String ATTR_MAX = "max";
public static final String ATTR_ALL_OF = "allOf";
public static final String ATTR_ANY_OF = "anyOf";
public static final String ATTR_CONDITIONAL = "conditional";
public static final String SECURITY_EXCEPTION = "java.lang.SecurityException";
public static final String FINDBUGS_ANNOTATIONS_CHECK_RETURN_VALUE =
"edu.umd.cs.findbugs.annotations.CheckReturnValue";
public static final String JAVAX_ANNOTATION_CHECK_RETURN_VALUE =
"javax.annotation.CheckReturnValue";
public static final String ERRORPRONE_CAN_IGNORE_RETURN_VALUE =
"com.google.errorprone.annotations.CanIgnoreReturnValue";
public static final String GUAVA_VISIBLE_FOR_TESTING =
"com.google.common.annotations.VisibleForTesting";
public static final Implementation IMPLEMENTATION =
new Implementation(AnnotationDetector.class, Scope.JAVA_FILE_SCOPE);
/** Placing SuppressLint on a local variable doesn't work for class-file based checks */
public static final Issue INSIDE_METHOD =
Issue.create(
"LocalSuppress",
"@SuppressLint on invalid element",
"The `@SuppressAnnotation` is used to suppress Lint warnings in Java files. However, "
+ "while many lint checks analyzes the Java source code, where they can find "
+ "annotations on (for example) local variables, some checks are analyzing the "
+ "`.class` files. And in class files, annotations only appear on classes, fields "
+ "and methods. Annotations placed on local variables disappear. If you attempt "
+ "to suppress a lint error for a class-file based lint check, the suppress "
+ "annotation not work. You must move the annotation out to the surrounding method.",
Category.CORRECTNESS,
3,
Severity.ERROR,
IMPLEMENTATION);
/** Incorrectly using a support annotation */
@SuppressWarnings("WeakerAccess")
public static final Issue ANNOTATION_USAGE =
Issue.create(
"SupportAnnotationUsage",
"Incorrect support annotation usage",
"This lint check makes sure that the support annotations (such as "
+ "`@IntDef` and `@ColorInt`) are used correctly. For example, it's an "
+ "error to specify an `@IntRange` where the `from` value is higher than "
+ "the `to` value.",
Category.CORRECTNESS,
2,
Severity.ERROR,
IMPLEMENTATION);
/** IntDef annotations should be unique */
public static final Issue UNIQUE =
Issue.create(
"UniqueConstants",
"Overlapping Enumeration Constants",
"The `@IntDef` annotation allows you to "
+ "create a light-weight \"enum\" or type definition. However, it's possible to "
+ "accidentally specify the same value for two or more of the values, which can "
+ "lead to hard-to-detect bugs. This check looks for this scenario and flags any "
+ "repeated constants.\n"
+ "\n"
+ "In some cases, the repeated constant is intentional (for example, renaming a "
+ "constant to a more intuitive name, and leaving the old name in place for "
+ "compatibility purposes). In that case, simply suppress this check by adding a "
+ "`@SuppressLint(\"UniqueConstants\")` annotation.",
Category.CORRECTNESS,
3,
Severity.ERROR,
IMPLEMENTATION)
.setAndroidSpecific(true);
/** Flags should typically be specified as bit shifts */
public static final Issue FLAG_STYLE =
Issue.create(
"ShiftFlags",
"Dangerous Flag Constant Declaration",
"When defining multiple constants for use in flags, the recommended style is "
+ "to use the form `1 << 2`, `1 << 3`, `1 << 4` and so on to ensure that the "
+ "constants are unique and non-overlapping.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
/** All IntDef constants should be included in switch */
public static final Issue SWITCH_TYPE_DEF =
Issue.create(
"SwitchIntDef",
"Missing @IntDef in Switch",
"This check warns if a `switch` statement does not explicitly include all "
+ "the values declared by the typedef `@IntDef` declaration.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION)
.setAndroidSpecific(true);
/** Constructs a new {@link AnnotationDetector} check */
public AnnotationDetector() {}
// ---- implements SourceCodeScanner ----
/**
* Set of fields we've already warned about {@link #FLAG_STYLE} for; these can be referenced
* multiple times, so we should only flag them once
*/
private Set<PsiElement> mWarnedFlags;
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<>(2);
types.add(UAnnotation.class);
types.add(USwitchExpression.class);
return types;
}
@Nullable
@Override
public UElementHandler createUastHandler(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private class AnnotationChecker extends UElementHandler {
private final JavaContext mContext;
public AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public void visitAnnotation(@NonNull UAnnotation annotation) {
String type = annotation.getQualifiedName();
if (type == null || type.startsWith("java.lang.")) {
return;
}
if (FQCN_SUPPRESS_LINT.equals(type)) {
UElement parent = annotation.getUastParent();
if (parent == null) {
return;
}
// Only flag local variables and parameters (not classes, fields and methods)
if (!(parent instanceof UDeclarationsExpression
|| parent instanceof ULocalVariable
|| parent instanceof UParameter)) {
return;
}
List<UNamedExpression> attributes = annotation.getAttributeValues();
if (attributes.size() == 1) {
UNamedExpression attribute = attributes.get(0);
UExpression value = attribute.getExpression();
if (value instanceof ULiteralExpression) {
Object v = ((ULiteralExpression) value).getValue();
if (v instanceof String) {
String id = (String) v;
checkSuppressLint(annotation, id);
}
} else if (UastExpressionUtils.isArrayInitializer(value)) {
for (UExpression ex : ((UCallExpression) value).getValueArguments()) {
if (ex instanceof ULiteralExpression) {
Object v = ((ULiteralExpression) ex).getValue();
if (v instanceof String) {
String id = (String) v;
if (!checkSuppressLint(annotation, id)) {
return;
}
}
}
}
}
}
} else if (SUPPORT_ANNOTATIONS_PREFIX.isPrefix(type)) {
if (CHECK_RESULT_ANNOTATION.isEquals(type)) {
// Check that the return type of this method is not void!
if (annotation.getUastParent() instanceof UMethod) {
UMethod method = (UMethod) annotation.getUastParent();
if (!method.isConstructor()
&& PsiType.VOID.equals(method.getReturnType())) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"@CheckResult should not be specified on `void` methods");
}
}
} else if (INT_RANGE_ANNOTATION.isEquals(type)
|| FLOAT_RANGE_ANNOTATION.isEquals(type)) {
// Check that the annotated element's type is int or long.
// Also make sure that from <= to.
boolean invalid;
if (INT_RANGE_ANNOTATION.isEquals(type)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
long from =
getLongAttribute(mContext, annotation, ATTR_FROM, Long.MIN_VALUE);
long to = getLongAttribute(mContext, annotation, ATTR_TO, Long.MAX_VALUE);
invalid = from > to;
} else {
checkTargetType(annotation, TYPE_FLOAT, TYPE_DOUBLE, true);
double from =
getDoubleAttribute(
mContext, annotation, ATTR_FROM, Double.NEGATIVE_INFINITY);
double to =
getDoubleAttribute(
mContext, annotation, ATTR_TO, Double.POSITIVE_INFINITY);
invalid = from > to;
}
if (invalid) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"Invalid range: the `from` attribute must be less than "
+ "the `to` attribute");
}
} else if (SIZE_ANNOTATION.isEquals(type)) {
// Check that the annotated element's type is an array, or a collection
// (or at least not an int or long; if so, suggest IntRange)
// Make sure the size and the modulo is not negative.
int unset = -42;
long exact = getLongAttribute(mContext, annotation, ATTR_VALUE, unset);
long min = getLongAttribute(mContext, annotation, ATTR_MIN, Long.MIN_VALUE);
long max = getLongAttribute(mContext, annotation, ATTR_MAX, Long.MAX_VALUE);
long multiple = getLongAttribute(mContext, annotation, ATTR_MULTIPLE, 1);
if (min > max) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"Invalid size range: the `min` attribute must be less than "
+ "the `max` attribute");
} else if (multiple < 1) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"The size multiple must be at least 1");
} else if (exact < 0 && exact != unset || min < 0 && min != Long.MIN_VALUE) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"The size can't be negative");
}
} else if (COLOR_INT_ANNOTATION.isEquals(type)) {
// Check that ColorInt applies to the right type
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (DIMENSION_ANNOTATION.isEquals(type) || (PX_ANNOTATION.isEquals(type))) {
// Check that @Dimension and @Px applies to the right type
checkTargetType(annotation, TYPE_INT, TYPE_LONG, TYPE_FLOAT, TYPE_DOUBLE, true);
} else if (INT_DEF_ANNOTATION.isEquals(type)
|| LONG_DEF_ANNOTATION.isEquals(type)) {
// Make sure IntDef constants are unique
ensureUniqueValues(annotation);
} else if (PERMISSION_ANNOTATION.isEquals(type)
|| PERMISSION_ANNOTATION_READ.isEquals(type)
|| PERMISSION_ANNOTATION_WRITE.isEquals(type)) {
// Check that if there are no arguments, this is specified on a parameter,
// and conversely, on methods and fields there is a valid argument.
if (annotation.getUastParent() instanceof UMethod) {
String value = getAnnotationStringValue(annotation, ATTR_VALUE);
String[] anyOf = getAnnotationStringValues(annotation, ATTR_ANY_OF);
String[] allOf = getAnnotationStringValues(annotation, ATTR_ALL_OF);
int set = 0;
//noinspection VariableNotUsedInsideIf
if (value != null) {
set++;
}
//noinspection VariableNotUsedInsideIf
if (allOf != null) {
set++;
}
//noinspection VariableNotUsedInsideIf
if (anyOf != null) {
set++;
}
if (set == 0) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"For methods, permission annotation should specify one "
+ "of `value`, `anyOf` or `allOf`");
} else if (set > 1) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"Only specify one of `value`, `anyOf` or `allOf`");
}
}
} else if (HALF_FLOAT_ANNOTATION.isEquals(type)) {
// Check that half floats are on shorts
checkTargetType(annotation, TYPE_SHORT, null, true);
} else if (type.endsWith(RES_SUFFIX)) {
// Check that resource type annotations are on ints
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (RESTRICT_TO_ANNOTATION.isEquals(type)) {
UExpression attributeValue = annotation.findDeclaredAttributeValue(ATTR_VALUE);
if (attributeValue == null) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
if (attributeValue == null) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"Restrict to what? Expected at least one `RestrictTo.Scope` arguments.");
} else {
String values = attributeValue.asSourceString();
if (values.contains("SUBCLASSES")
&& annotation.getUastParent() instanceof UClass) {
mContext.report(
ANNOTATION_USAGE,
annotation,
mContext.getLocation(annotation),
"`RestrictTo.Scope.SUBCLASSES` should only be specified on methods and fields");
}
}
}
} else {
// Look for typedefs (and make sure they're specified on the right type)
PsiElement resolved = annotation.resolve();
if (resolved != null) {
PsiClass cls = (PsiClass) resolved;
if (cls.isAnnotationType() && cls.getModifierList() != null) {
for (PsiAnnotation a :
mContext.getEvaluator().getAllAnnotations(cls, false)) {
String name = a.getQualifiedName();
if (INT_DEF_ANNOTATION.isEquals(name)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (LONG_DEF_ANNOTATION.isEquals(name)) {
checkTargetType(annotation, TYPE_LONG, null, true);
} else if (STRING_DEF_ANNOTATION.isEquals(type)) {
checkTargetType(annotation, TYPE_STRING, null, true);
}
}
}
}
}
}
private void checkTargetType(
@NonNull UAnnotation node, @NonNull String type, boolean allowCollection) {
checkTargetType(node, type, null, null, null, allowCollection);
}
private void checkTargetType(
@NonNull UAnnotation node,
@NonNull String type1,
@Nullable String type2,
boolean allowCollection) {
checkTargetType(node, type1, type2, null, null, allowCollection);
}
private void checkTargetType(
@NonNull UAnnotation node,
@NonNull String type1,
@Nullable String type2,
@Nullable String type3,
@Nullable String type4,
boolean allowCollection) {
UElement parent = node.getUastParent();
PsiType type;
if (parent instanceof UDeclarationsExpression) {
List<UDeclaration> elements = ((UDeclarationsExpression) parent).getDeclarations();
if (!elements.isEmpty()) {
UDeclaration element = elements.get(0);
if (element instanceof ULocalVariable) {
type = ((ULocalVariable) element).getType();
} else {
return;
}
} else {
return;
}
} else if (parent instanceof UMethod) {
UMethod method = (UMethod) parent;
type =
method.isConstructor()
? mContext.getEvaluator()
.getClassType(UastUtils.getContainingUClass(method))
: method.getReturnType();
} else if (parent instanceof UVariable) {
// Field or local variable or parameter
UVariable variable = (UVariable) parent;
if (variable.getTypeReference() == null) {
// Uh oh.
// https://youtrack.jetbrains.com/issue/KT-20172
return;
}
type = variable.getType();
} else {
return;
}
if (type == null) {
return;
}
if (allowCollection) {
if (type instanceof PsiArrayType) {
// For example, int[]
type = type.getDeepComponentType();
} else if (type instanceof PsiClassType) {
// For example, List<Integer>
PsiClassType classType = (PsiClassType) type;
if (classType.getParameters().length == 1) {
PsiClass resolved = classType.resolve();
if (resolved != null
&& mContext.getEvaluator()
.implementsInterface(
resolved, "java.util.Collection", false)) {
type = classType.getParameters()[0];
}
}
}
}
if (!type.isValid()) {
return;
}
String typeName = type.getCanonicalText();
if (typeName.equals("error.NonExistentClass")) {
// Type not found. Not awesome.
// https://youtrack.jetbrains.com/issue/KT-20172
return;
}
if (!(typeName.equals(type1)
|| typeName.equals(type2)
|| typeName.equals(type3)
|| typeName.equals(type4))) {
// Autoboxing? You can put @DrawableRes on a java.lang.Integer for example
if (typeName.equals(getAutoBoxedType(type1))
|| type2 != null && typeName.equals(getAutoBoxedType(type2))
|| type3 != null && typeName.equals(getAutoBoxedType(type3))
|| type4 != null && typeName.equals(getAutoBoxedType(type4))) {
return;
}
String expectedTypes;
if (type4 != null) {
expectedTypes = type1 + ", " + type2 + ", " + type3 + ", or " + type4;
} else if (type3 != null) {
expectedTypes = type1 + ", " + type2 + ", or " + type3;
} else if (type2 != null) {
expectedTypes = type1 + " or " + type2;
} else {
expectedTypes = type1;
}
if (typeName.equals(TYPE_STRING)) {
typeName = "String";
}
String message =
String.format(
"This annotation does not apply for type %1$s; expected %2$s",
typeName, expectedTypes);
Location location = mContext.getLocation(node);
mContext.report(ANNOTATION_USAGE, node, location, message);
}
}
@Override
public void visitSwitchExpression(@NonNull USwitchExpression switchExpression) {
UExpression condition = switchExpression.getExpression();
if (condition != null && PsiType.INT.equals(condition.getExpressionType())) {
UAnnotation annotation = findIntDefAnnotation(condition);
if (annotation != null) {
UExpression value = annotation.findAttributeValue(ATTR_VALUE);
if (value == null) {
value = annotation.findAttributeValue(null);
}
if (value != null && UastExpressionUtils.isArrayInitializer(value)) {
List<UExpression> allowedValues =
((UCallExpression) value).getValueArguments();
switchExpression.accept(new SwitchChecker(switchExpression, allowedValues));
}
}
}
}
/**
* Searches for the corresponding @IntDef annotation definition associated with a given node
*/
@Nullable
private UAnnotation findIntDefAnnotation(@NonNull UExpression expression) {
if (expression instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) expression).resolve();
if (resolved instanceof PsiModifierListOwner) {
PsiAnnotation[] annotations =
mContext.getEvaluator()
.getAllAnnotations((PsiModifierListOwner) resolved, true);
PsiAnnotation[] relevantAnnotations =
filterRelevantAnnotations(mContext.getEvaluator(), annotations);
UAnnotation annotation =
TypedefDetector.Companion.findIntDef(
JavaUAnnotation.wrap(relevantAnnotations));
if (annotation != null) {
return annotation;
}
}
if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
UExpression lastAssignment =
UastLintUtils.findLastAssignment(variable, expression);
if (lastAssignment != null) {
return findIntDefAnnotation(lastAssignment);
}
}
} else if (expression instanceof UCallExpression) {
PsiMethod method = ((UCallExpression) expression).resolve();
if (method != null) {
PsiAnnotation[] annotations =
mContext.getEvaluator().getAllAnnotations(method, true);
PsiAnnotation[] relevantAnnotations =
filterRelevantAnnotations(mContext.getEvaluator(), annotations);
List<UAnnotation> uAnnotations = JavaUAnnotation.wrap(relevantAnnotations);
UAnnotation annotation = TypedefDetector.Companion.findIntDef(uAnnotations);
if (annotation != null) {
return annotation;
}
}
} else if (expression instanceof UIfExpression) {
UIfExpression ifExpression = (UIfExpression) expression;
if (ifExpression.getThenExpression() != null) {
UAnnotation result = findIntDefAnnotation(ifExpression.getThenExpression());
if (result != null) {
return result;
}
}
if (ifExpression.getElseExpression() != null) {
UAnnotation result = findIntDefAnnotation(ifExpression.getElseExpression());
if (result != null) {
return result;
}
}
} else if (expression instanceof JavaUTypeCastExpression) {
return findIntDefAnnotation(((JavaUTypeCastExpression) expression).getOperand());
} else if (expression instanceof UParenthesizedExpression) {
return findIntDefAnnotation(
((UParenthesizedExpression) expression).getExpression());
}
return null;
}
@Nullable
private Integer getConstantValue(@NonNull PsiField intDefConstantRef) {
Object constant = intDefConstantRef.computeConstantValue();
if (constant instanceof Number) {
return ((Number) constant).intValue();
}
return null;
}
private void ensureUniqueValues(@NonNull UAnnotation node) {
UExpression value = node.findDeclaredAttributeValue(ATTR_VALUE);
if (value == null) {
value = node.findDeclaredAttributeValue(null);
}
if (value == null) {
return;
}
if (!(UastExpressionUtils.isArrayInitializer(value))) {
return;
}
List<UExpression> initializers = ((UCallExpression) value).getValueArguments();
Map<Number, Integer> valueToIndex =
Maps.newHashMapWithExpectedSize(initializers.size());
boolean flag = getAnnotationBooleanValue(node, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE;
if (flag) {
ensureUsingFlagStyle(initializers);
}
ConstantEvaluator constantEvaluator = new ConstantEvaluator();
for (int index = 0; index < initializers.size(); index++) {
UExpression expression = initializers.get(index);
Object o = constantEvaluator.evaluate(expression);
if (o instanceof Number) {
Number number = (Number) o;
if (valueToIndex.containsKey(number)) {
@SuppressWarnings("UnnecessaryLocalVariable")
Number repeatedValue = number;
Location location;
String message;
int prevIndex = valueToIndex.get(number);
UExpression prevConstant = initializers.get(prevIndex);
message =
String.format(
"Constants `%1$s` and `%2$s` specify the same exact "
+ "value (%3$s); this is usually a cut & paste or "
+ "merge error",
expression.asSourceString(),
prevConstant.asSourceString(),
repeatedValue.toString());
location = mContext.getLocation(expression);
Location secondary = mContext.getLocation(prevConstant);
secondary.setMessage("Previous same value");
location.setSecondary(secondary);
UElement scope = getAnnotationScope(node);
mContext.report(UNIQUE, scope, location, message);
break;
}
valueToIndex.put(number, index);
}
}
}
private void ensureUsingFlagStyle(@NonNull List<UExpression> constants) {
if (constants.size() < 3) {
return;
}
for (UExpression constant : constants) {
if (constant instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) constant).resolve();
// Don't try to check complied code.
if (!(resolved instanceof PsiCompiledElement) && resolved instanceof PsiField) {
UExpression initializer =
UastFacade.INSTANCE.getInitializerBody((PsiField) resolved);
if (initializer instanceof ULiteralExpression) {
ULiteralExpression literal = (ULiteralExpression) initializer;
Object o = literal.getValue();
if (!(o instanceof Number)) {
continue;
}
long value = ((Number) o).longValue();
// Allow -1, 0 and 1. You can write 1 as "1 << 0" but IntelliJ for
// example warns that that's a redundant shift.
if (Math.abs(value) <= 1) {
continue;
}
// Only warn if we're setting a specific bit
if (Long.bitCount(value) != 1) {
continue;
}
int shift = Long.numberOfTrailingZeros(value);
if (mWarnedFlags == null) {
mWarnedFlags = Sets.newHashSet();
}
if (!mWarnedFlags.add(resolved)) {
return;
}
String message =
String.format(
Locale.US,
"Consider declaring this constant using 1 << %1$d instead",
shift);
String replace =
String.format(
Locale.ROOT,
"1%s << %d",
o instanceof Long ? "L" : "",
shift);
LintFix fix =
fix().replace()
.sharedName("Change declaration to <<")
.with(replace)
.autoFix()
.build();
Location location = mContext.getLocation(initializer);
mContext.report(FLAG_STYLE, initializer, location, message, fix);
}
}
}
}
}
private boolean checkSuppressLint(@NonNull UAnnotation node, @NonNull String id) {
IssueRegistry registry = mContext.getDriver().getRegistry();
Issue issue = registry.getIssue(id);
// Special-case the ApiDetector issue, since it does both source file analysis
// only on field references, and class file analysis on the rest, so we allow
// annotations outside of methods only on fields
if (issue != null && !issue.getImplementation().getScope().contains(Scope.JAVA_FILE)
|| issue == ApiDetector.UNSUPPORTED) {
// This issue doesn't have AST access: annotations are not
// available for local variables or parameters
UElement scope = getAnnotationScope(node);
mContext.report(
INSIDE_METHOD,
scope,
mContext.getLocation(node),
String.format(
"The `@SuppressLint` annotation cannot be used on a local "
+ "variable with the lint check '%1$s': move out to the "
+ "surrounding method",
id));
return false;
}
return true;
}
private class SwitchChecker extends AbstractUastVisitor {
private final USwitchExpression mSwitchExpression;
private final List<UExpression> mAllowedValues;
private final List<Object> mFields;
private final List<Integer> mSeenValues;
private boolean mReported = false;
private SwitchChecker(
USwitchExpression switchExpression, List<UExpression> allowedValues) {
mSwitchExpression = switchExpression;
mAllowedValues = allowedValues;
mFields = Lists.newArrayListWithCapacity(allowedValues.size());
for (UExpression allowedValue : allowedValues) {
if (allowedValue instanceof ExternalReferenceExpression) {
ExternalReferenceExpression externalRef =
(ExternalReferenceExpression) allowedValue;
PsiElement resolved = UastLintUtils.resolve(externalRef, switchExpression);
if (resolved instanceof PsiField) {
mFields.add(resolved);
}
} else if (allowedValue instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) allowedValue).resolve();
if (resolved != null) {
mFields.add(resolved);
}
} else if (allowedValue instanceof ULiteralExpression) {
mFields.add(allowedValue);
}
}
mSeenValues = Lists.newArrayListWithCapacity(allowedValues.size());
}
@Override
public boolean visitSwitchClauseExpression(USwitchClauseExpression node) {
if (mReported) {
return true;
}
if (mAllowedValues == null) {
return true;
}
List<UExpression> caseValues = node.getCaseValues();
if (caseValues.isEmpty()) {
// We had an else clause: don't report any as missing
mFields.clear();
return true;
}
for (UExpression caseValue : caseValues) {
if (caseValue instanceof ULiteralExpression) {
// Report warnings if you specify hardcoded constants.
// It's the wrong thing to do.
List<String> list = computeFieldNames(mSwitchExpression, mAllowedValues);
// Keep error message in sync with {@link #getMissingCases}
String message =
"Don't use a constant here; expected one of: "
+ displayConstants(list);
mContext.report(
SWITCH_TYPE_DEF,
caseValue,
mContext.getLocation(caseValue),
message);
// Don't look for other missing typedef constants since you might
// have aliased with value
mReported = true;
} else if (caseValue
instanceof
UReferenceExpression) { // default case can have null expression
PsiElement resolved = ((UReferenceExpression) caseValue).resolve();
if (resolved == null) {
// If there are compilation issues (e.g. user is editing code) we
// can't be certain, so don't flag anything.
return true;
}
if (resolved instanceof PsiField) {
// We can't just do
// fields.remove(resolved);
// since the fields list contains instances of potentially
// different types with different hash codes (due to the
// external annotations, which are not of the same type as
// for example the ECJ based ones.
//
// The equals method on external field class deliberately handles
// this (but it can't make its hash code match what
// the ECJ fields do, which is tied to the ECJ binding hash code.)
// So instead, manually check for equals. These lists tend to
// be very short anyway.
PsiField resolvedField = (PsiField) resolved;
boolean found = removeFieldFromList(mFields, resolvedField);
if (!found) {
// Look for local alias
UExpression initializer =
UastFacade.INSTANCE.getInitializerBody(
((PsiField) resolved));
if (initializer instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) initializer).resolve();
if (resolved instanceof PsiField) {
found = removeFieldFromList(mFields, (PsiField) resolved);
}
}
}
if (found) {
Integer cv = getConstantValue((PsiField) resolved);
if (cv != null) {
mSeenValues.add(cv);
}
} else {
List<String> list =
computeFieldNames(mSwitchExpression, mAllowedValues);
// Keep error message in sync with {@link #getMissingCases}
String message =
"Unexpected constant; expected one of: "
+ displayConstants(list);
LintFix fix = fix().data(list);
Location location = mContext.getNameLocation(caseValue);
mContext.report(SWITCH_TYPE_DEF, caseValue, location, message, fix);
}
}
}
}
return true;
}
@Override
public void afterVisitSwitchExpression(USwitchExpression node) {
reportMissingSwitchCases();
super.afterVisitSwitchExpression(node);
}
private void reportMissingSwitchCases() {
if (mReported) {
return;
}
if (mAllowedValues == null) {
return;
}
// Any missing switch constants? Before we flag them, look to see if any
// of them have the same values: those can be omitted
if (!mFields.isEmpty()) {
ListIterator<Object> iterator = mFields.listIterator();
while (iterator.hasNext()) {
Object next = iterator.next();
if (next instanceof PsiField) {
Integer cv = getConstantValue((PsiField) next);
if (mSeenValues.contains(cv)) {
iterator.remove();
}
}
}
}
if (!mFields.isEmpty()) {
List<String> list = computeFieldNames(mSwitchExpression, mFields);
// Keep error message in sync with {@link #getMissingCases}
LintFix fix = fix().data(list);
UIdentifier identifier = mSwitchExpression.getSwitchIdentifier();
Location location = mContext.getLocation(identifier);
// Workaround Kotlin UAST passing <error> instead of PsiKeyword as in Java
if (mSwitchExpression instanceof KotlinUSwitchExpression
&& !"when".equals(identifier.getName())) {
PsiElement sourcePsi = mSwitchExpression.getSourcePsi();
if (sourcePsi != null) {
PsiElement keyword = sourcePsi.getFirstChild();
if (keyword != null) {
location = mContext.getLocation(keyword);
}
}
}
String message =
"Switch statement on an `int` with known associated constant "
+ "missing case "
+ displayConstants(list);
mContext.report(SWITCH_TYPE_DEF, mSwitchExpression, location, message, fix);
}
}
}
}
@NonNull
private static String displayConstants(List<String> list) {
return CollectionsKt.joinToString(
list,
", ", // separator
"", // prefix
"", // postfix
-1, // limited
"", // truncated
s -> {
int index = s.lastIndexOf('.');
if (index != -1) {
int classIndex = s.lastIndexOf('.', index - 1);
if (classIndex != -1) {
return "`" + s.substring(classIndex + 1) + "`";
}
}
return "`" + s + "`";
});
}
private static List<String> computeFieldNames(
@NonNull USwitchExpression node, Iterable<?> allowedValues) {
List<String> list = Lists.newArrayList();
for (Object o : allowedValues) {
if (o instanceof ExternalReferenceExpression) {
ExternalReferenceExpression externalRef = (ExternalReferenceExpression) o;
PsiElement resolved = UastLintUtils.resolve(externalRef, node);
if (resolved != null) {
o = resolved;
}
} else if (o instanceof PsiReferenceExpression) {
PsiReferenceExpression ref = (PsiReferenceExpression) o;
PsiElement resolved = ref.resolve();
if (resolved != null) {
o = resolved;
} else {
String referenceName = ref.getReferenceName();
if (referenceName != null) {
list.add(referenceName);
}
continue;
}
} else if (o instanceof PsiLiteral) {
list.add((String) ((PsiLiteral) o).getValue());
continue;
} else if (o instanceof UReferenceExpression) {
UReferenceExpression ref = (UReferenceExpression) o;
PsiElement resolved = ref.resolve();
if (resolved == null) {
String resolvedName = ref.getResolvedName();
if (resolvedName != null) {
list.add(resolvedName);
}
continue;
}
o = resolved;
}
if (o instanceof PsiField) {
PsiField field = (PsiField) o;
// Only include class name if necessary
String name = field.getName();
UClass clz = UastUtils.getParentOfType(node, UClass.class, true);
if (clz != null) {
PsiClass containingClass = field.getContainingClass();
if (containingClass != null && !containingClass.isEquivalentTo(clz.getPsi())) {
name = containingClass.getQualifiedName() + '.' + field.getName();
}
}
list.add(name);
}
}
Collections.sort(list);
return list;
}
/**
* Returns the node to use as the scope for the given annotation node. You can't annotate an
* annotation itself (with {@code @SuppressLint}), but you should be able to place an annotation
* next to it, as a sibling, to only suppress the error on this annotated element, not the whole
* surrounding class.
*/
@NonNull
private static UElement getAnnotationScope(@NonNull UAnnotation node) {
UElement scope = UastUtils.getParentOfType(node, UAnnotation.class, true);
if (scope == null) {
scope = node;
}
return scope;
}
private static boolean removeFieldFromList(
@NonNull List<Object> fields, @NonNull PsiField resolvedField) {
for (Object field : fields) {
// We can't just call .equals here because the annotation
// we are comparing against may be either a PsiFieldImpl
// (for a local annotation) or a ClsFieldImpl (for an annotation
// read from storage) or maybe even other PSI internal classes.
// So compare by name and class instead.
if (!(field instanceof PsiField)) {
continue;
}
PsiField candidateField = (PsiField) field;
if (candidateField.isEquivalentTo(resolvedField)) {
return true;
}
}
return false;
}
// Like JavaEvaluator#filterRelevantAnnotations, but hardcoded for the IntRange and
// IntDef annotations since this check isn't a generalized annotation checker like the
// others.
@NonNull
static PsiAnnotation[] filterRelevantAnnotations(
@NonNull JavaEvaluator evaluator, @NonNull PsiAnnotation[] annotations) {
List<PsiAnnotation> result = null;
int length = annotations.length;
if (length == 0) {
return annotations;
}
for (PsiAnnotation annotation : annotations) {
String signature = annotation.getQualifiedName();
if (signature == null || signature.startsWith("java.")) {
// @Override, @SuppressWarnings etc. Ignore
continue;
}
if (SUPPORT_ANNOTATIONS_PREFIX.isPrefix(signature)
|| signature.equals(GMS_HIDE_ANNOTATION)) {
// Bail on the nullness annotations early since they're the most commonly
// defined ones. They're not analyzed in lint yet.
if (signature.endsWith(".Nullable") || signature.endsWith(".NonNull")) {
continue;
}
// Common case: there's just one annotation; no need to create a list copy
if (length == 1) {
return annotations;
}
if (result == null) {
result = new ArrayList<>(2);
}
result.add(annotation);
}
// Special case @IntDef and @StringDef: These are used on annotations
// themselves. For example, you create a new annotation named @foo.bar.Baz,
// annotate it with @IntDef, and then use @foo.bar.Baz in your signatures.
// Here we want to map from @foo.bar.Baz to the corresponding int def.
// Don't need to compute this if performing @IntDef or @StringDef lookup
PsiClass cls = null;
PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement();
if (ref != null) {
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiClass) {
cls = (PsiClass) resolved;
}
} else {
Project project = annotation.getProject();
GlobalSearchScope scope = GlobalSearchScope.projectScope(project);
cls = JavaPsiFacade.getInstance(project).findClass(signature, scope);
}
if (cls == null || !cls.isAnnotationType()) {
continue;
}
PsiAnnotation[] innerAnnotations = evaluator.getAllAnnotations(cls, false);
for (int j = 0; j < innerAnnotations.length; j++) {
PsiAnnotation inner = innerAnnotations[j];
String a = inner.getQualifiedName();
if (a == null || a.startsWith("java.")) {
// @Override, @SuppressWarnings etc. Ignore
continue;
}
if (INT_DEF_ANNOTATION.isEquals(a)
|| LONG_DEF_ANNOTATION.isEquals(a)
|| PERMISSION_ANNOTATION.isEquals(a)
|| INT_RANGE_ANNOTATION.isEquals(a)
|| STRING_DEF_ANNOTATION.isEquals(a)) {
if (length == 1 && j == innerAnnotations.length - 1 && result == null) {
return innerAnnotations;
}
if (result == null) {
result = new ArrayList<>(2);
}
result.add(inner);
}
}
}
return result != null
? result.toArray(PsiAnnotation.EMPTY_ARRAY)
: PsiAnnotation.EMPTY_ARRAY;
}
}