com.android.tools.lint.checks.AnnotationDetector Maven / Gradle / Ivy
/*
* 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.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.checks.PermissionRequirement.getAnnotationBooleanValue;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ALL_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ANY_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_FROM;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_MAX;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_MIN;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_MULTIPLE;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_TO;
import static com.android.tools.lint.checks.SupportAnnotationDetector.CHECK_RESULT_ANNOTATION;
import static com.android.tools.lint.checks.SupportAnnotationDetector.FLOAT_RANGE_ANNOTATION;
import static com.android.tools.lint.checks.SupportAnnotationDetector.INT_RANGE_ANNOTATION;
import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION;
import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ;
import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE;
import static com.android.tools.lint.checks.SupportAnnotationDetector.SIZE_ANNOTATION;
import static com.android.tools.lint.checks.SupportAnnotationDetector.filterRelevantAnnotations;
import static com.android.tools.lint.checks.SupportAnnotationDetector.getDoubleAttribute;
import static com.android.tools.lint.checks.SupportAnnotationDetector.getLongAttribute;
import static com.android.tools.lint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.lint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.lint.detector.api.LintUtils.findSubstring;
import static com.android.tools.lint.detector.api.LintUtils.getAutoBoxedType;
import static com.android.tools.lint.detector.api.ResourceEvaluator.COLOR_INT_ANNOTATION;
import static com.android.tools.lint.detector.api.ResourceEvaluator.PX_ANNOTATION;
import static com.android.tools.lint.detector.api.ResourceEvaluator.RES_SUFFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.JavaEvaluator;
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.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiAnnotationOwner;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.PsiArrayType;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
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.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNameValuePair;
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.PsiSwitchLabelStatement;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Checks annotations to make sure they are valid
*/
public class AnnotationDetector extends Detector implements JavaPsiScanner {
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", //$NON-NLS-1$
"@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", //$NON-NLS-1$
"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", //$NON-NLS-1$
"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);
/** Flags should typically be specified as bit shifts */
public static final Issue FLAG_STYLE = Issue.create(
"ShiftFlags", //$NON-NLS-1$
"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", //$NON-NLS-1$
"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);
/** Constructs a new {@link AnnotationDetector} check */
public AnnotationDetector() {
}
// ---- Implements JavaScanner ----
/**
* 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 mWarnedFlags;
@Override
public List> getApplicablePsiTypes() {
List> types = new ArrayList<>(2);
types.add(PsiAnnotation.class);
types.add(PsiSwitchStatement.class);
return types;
}
@Nullable
@Override
public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private class AnnotationChecker extends JavaElementVisitor {
private final JavaContext mContext;
public AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public void visitAnnotation(PsiAnnotation annotation) {
String type = annotation.getQualifiedName();
if (type == null || type.startsWith("java.lang.")) {
return;
}
if (FQCN_SUPPRESS_LINT.equals(type)) {
PsiAnnotationOwner owner = annotation.getOwner();
if (owner == null) {
return;
}
if (owner instanceof PsiModifierList) {
PsiElement parent = ((PsiModifierList) owner).getParent();
// Only flag local variables and parameters (not classes, fields and methods)
if (!(parent instanceof PsiDeclarationStatement
|| parent instanceof PsiLocalVariable
|| parent instanceof PsiParameter)) {
return;
}
} else {
return;
}
PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes();
if (attributes.length == 1) {
PsiNameValuePair attribute = attributes[0];
PsiAnnotationMemberValue value = attribute.getValue();
if (value instanceof PsiLiteral) {
Object v = ((PsiLiteral) value).getValue();
if (v instanceof String) {
String id = (String) v;
checkSuppressLint(annotation, id);
}
} else if (value instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue initializer =
(PsiArrayInitializerMemberValue) value;
for (PsiAnnotationMemberValue expression : initializer.getInitializers()) {
if (expression instanceof PsiLiteral) {
Object v = ((PsiLiteral) expression).getValue();
if (v instanceof String) {
String id = (String) v;
if (!checkSuppressLint(annotation, id)) {
return;
}
}
}
}
}
}
} else if (type.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
if (CHECK_RESULT_ANNOTATION.equals(type)) {
// Check that the return type of this method is not void!
if (annotation.getParent() instanceof PsiModifierList
&& annotation.getParent().getParent() instanceof PsiMethod) {
PsiMethod method = (PsiMethod) annotation.getParent().getParent();
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.equals(type)
|| FLOAT_RANGE_ANNOTATION.equals(type)) {
// Check that the annotated element's type is int or long.
// Also make sure that from <= to.
boolean invalid;
if (INT_RANGE_ANNOTATION.equals(type)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
long from = getLongAttribute(annotation, ATTR_FROM, Long.MIN_VALUE);
long to = getLongAttribute(annotation, ATTR_TO, Long.MAX_VALUE);
invalid = from > to;
} else {
checkTargetType(annotation, TYPE_FLOAT, TYPE_DOUBLE, true);
double from = getDoubleAttribute(annotation, ATTR_FROM,
Double.NEGATIVE_INFINITY);
double to = getDoubleAttribute(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.equals(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(annotation, ATTR_VALUE, unset);
long min = getLongAttribute(annotation, ATTR_MIN, Long.MIN_VALUE);
long max = getLongAttribute(annotation, ATTR_MAX, Long.MAX_VALUE);
long multiple = getLongAttribute(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.equals(type) || (PX_ANNOTATION.equals(type))) {
// Check that ColorInt applies to the right type
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (INT_DEF_ANNOTATION.equals(type)) {
// Make sure IntDef constants are unique
ensureUniqueValues(annotation);
} else if (PERMISSION_ANNOTATION.equals(type) ||
PERMISSION_ANNOTATION_READ.equals(type) ||
PERMISSION_ANNOTATION_WRITE.equals(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.getParent() instanceof PsiModifierList
&& annotation.getParent().getParent() instanceof PsiMethod) {
String value = PermissionRequirement.getAnnotationStringValue(annotation, ATTR_VALUE);
String[] anyOf = PermissionRequirement.getAnnotationStringValues(annotation, ATTR_ANY_OF);
String[] allOf = PermissionRequirement.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 (type.endsWith(RES_SUFFIX)) {
// Check that resource type annotations are on ints
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
}
} else {
// Look for typedefs (and make sure they're specified on the right type)
PsiJavaCodeReferenceElement referenceElement = annotation
.getNameReferenceElement();
if (referenceElement != null) {
PsiElement resolved = referenceElement.resolve();
if (resolved instanceof PsiClass) {
PsiClass cls = (PsiClass) resolved;
if (cls.isAnnotationType() && cls.getModifierList() != null) {
for (PsiAnnotation a : cls.getModifierList().getAnnotations()) {
String name = a.getQualifiedName();
if (INT_DEF_ANNOTATION.equals(name)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (STRING_DEF_ANNOTATION.equals(type)) {
checkTargetType(annotation, TYPE_STRING, null, true);
}
}
}
}
}
}
}
private void checkTargetType(@NonNull PsiAnnotation node, @NonNull String type1,
@Nullable String type2, boolean allowCollection) {
PsiAnnotationOwner owner = node.getOwner();
if (owner instanceof PsiModifierList) {
PsiElement parent = ((PsiModifierList) owner).getParent();
PsiType type;
if (parent instanceof PsiDeclarationStatement) {
PsiElement[] elements = ((PsiDeclarationStatement) parent).getDeclaredElements();
if (elements.length > 0) {
PsiElement element = elements[0];
if (element instanceof PsiLocalVariable) {
type = ((PsiLocalVariable)element).getType();
} else {
return;
}
} else {
return;
}
} else if (parent instanceof PsiMethod) {
PsiMethod method = (PsiMethod) parent;
type = method.isConstructor()
? mContext.getEvaluator().getClassType(method.getContainingClass())
: method.getReturnType();
} else if (parent instanceof PsiVariable) {
// Field or local variable or parameter
type = ((PsiVariable)parent).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
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];
}
}
}
}
String typeName = type.getCanonicalText();
if (!typeName.equals(type1)
&& (type2 == null || !typeName.equals(type2))) {
// Autoboxing? You can put @DrawableRes on a java.lang.Integer for example
if (typeName.equals(getAutoBoxedType(type1))
|| type2 != null && typeName.equals(getAutoBoxedType(type2))) {
return;
}
String expectedTypes = type2 == null ? type1 : type1 + " or " + type2;
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 visitSwitchStatement(PsiSwitchStatement statement) {
PsiExpression condition = statement.getExpression();
if (condition != null && PsiType.INT.equals(condition.getType())) {
PsiAnnotation annotation = findIntDef(condition);
if (annotation != null) {
checkSwitch(statement, annotation);
}
}
}
/**
* Searches for the corresponding @IntDef annotation definition associated
* with a given node
*/
@Nullable
private PsiAnnotation findIntDef(@NonNull PsiElement node) {
if (node instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReference) node).resolve();
if (resolved instanceof PsiModifierListOwner) {
PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations(
(PsiModifierListOwner)resolved, true);
PsiAnnotation annotation = SupportAnnotationDetector.findIntDef(
filterRelevantAnnotations(mContext.getEvaluator(), annotations));
if (annotation != null) {
return annotation;
}
}
if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(node, 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) {
for (PsiElement element : ((PsiDeclarationStatement) prev)
.getDeclaredElements()) {
if (variable.equals(element)) {
PsiExpression initializer = variable.getInitializer();
if (initializer != null) {
return findIntDef(initializer);
}
break;
}
}
} 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) {
PsiExpression rExpression = assign.getRExpression();
if (rExpression != null) {
return findIntDef(rExpression);
}
break;
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
} else if (node instanceof PsiMethodCallExpression) {
PsiMethod method = ((PsiMethodCallExpression) node).resolveMethod();
if (method != null) {
JavaEvaluator evaluator = mContext.getEvaluator();
PsiAnnotation[] annotations = evaluator.getAllAnnotations(method, true);
PsiAnnotation annotation = SupportAnnotationDetector.findIntDef(
filterRelevantAnnotations(evaluator, annotations));
if (annotation != null) {
return annotation;
}
}
} else if (node instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) node;
if (expression.getThenExpression() != null) {
PsiAnnotation result = findIntDef(expression.getThenExpression());
if (result != null) {
return result;
}
}
if (expression.getElseExpression() != null) {
PsiAnnotation result = findIntDef(expression.getElseExpression());
if (result != null) {
return result;
}
}
} else if (node instanceof PsiTypeCastExpression) {
PsiTypeCastExpression cast = (PsiTypeCastExpression) node;
if (cast.getOperand() != null) {
return findIntDef(cast.getOperand());
}
} else if (node instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression expression = (PsiParenthesizedExpression) node;
if (expression.getExpression() != null) {
return findIntDef(expression.getExpression());
}
}
return null;
}
private void checkSwitch(@NonNull PsiSwitchStatement node, @NonNull PsiAnnotation annotation) {
PsiCodeBlock block = node.getBody();
if (block == null) {
return;
}
PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue(ATTR_VALUE);
if (value == null) {
value = annotation.findDeclaredAttributeValue(null);
}
if (value == null) {
return;
}
if (!(value instanceof PsiArrayInitializerMemberValue)) {
return;
}
PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue)value;
PsiAnnotationMemberValue[] allowedValues = array.getInitializers();
List fields = Lists.newArrayListWithCapacity(allowedValues.length);
List seenValues = Lists.newArrayListWithCapacity(allowedValues.length);
for (PsiAnnotationMemberValue allowedValue : allowedValues) {
if (allowedValue instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) allowedValue).resolve();
if (resolved != null) {
fields.add(resolved);
}
} else if (allowedValue instanceof PsiLiteral) {
fields.add(allowedValue);
}
}
// Empty switch: arguably we could skip these (since the IDE already warns about
// empty switches) but it's useful since the quickfix will kick in and offer all
// the missing ones when you're editing.
// if (block.getStatements().length == 0) { return; }
for (PsiStatement statement : block.getStatements()) {
if (statement instanceof PsiSwitchLabelStatement) {
PsiSwitchLabelStatement caseStatement = (PsiSwitchLabelStatement) statement;
PsiExpression expression = caseStatement.getCaseValue();
if (expression instanceof PsiLiteral) {
// Report warnings if you specify hardcoded constants.
// It's the wrong thing to do.
List list = computeFieldNames(node, Arrays.asList(allowedValues));
// Keep error message in sync with {@link #getMissingCases}
String message = "Don't use a constant here; expected one of: " + Joiner
.on(", ").join(list);
mContext.report(SWITCH_TYPE_DEF, expression,
mContext.getLocation(expression), message);
return; // Don't look for other missing typedef constants since you might
// have aliased with value
} else if (expression instanceof PsiReferenceExpression) { // default case can have null expression
PsiElement resolved = ((PsiReferenceExpression) expression).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;
}
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.
boolean found = false;
ListIterator iterator = fields.listIterator();
while (iterator.hasNext()) {
PsiElement field = iterator.next();
if (field.equals(resolved)) {
iterator.remove();
found = true;
break;
}
}
if (!found) {
// Look for local alias
PsiExpression initializer = ((PsiField) resolved).getInitializer();
if (initializer instanceof PsiReferenceExpression) {
resolved = ((PsiReferenceExpression) expression).resolve();
if (resolved instanceof PsiField) {
iterator = fields.listIterator();
while (iterator.hasNext()) {
PsiElement field = iterator.next();
if (field.equals(initializer)) {
iterator.remove();
found = true;
break;
}
}
}
}
}
if (found) {
Integer cv = getConstantValue((PsiField) resolved);
if (cv != null) {
seenValues.add(cv);
}
} else {
List list = computeFieldNames(node, Arrays.asList(allowedValues));
// Keep error message in sync with {@link #getMissingCases}
String message = "Unexpected constant; expected one of: " + Joiner
.on(", ").join(list);
Location location = mContext.getNameLocation(expression);
mContext.report(SWITCH_TYPE_DEF, expression, location, message);
}
}
}
}
}
// Any missing switch constants? Before we flag them, look to see if any
// of them have the same values: those can be omitted
if (!fields.isEmpty()) {
ListIterator iterator = fields.listIterator();
while (iterator.hasNext()) {
PsiElement next = iterator.next();
if (next instanceof PsiField) {
Integer cv = getConstantValue((PsiField)next);
if (seenValues.contains(cv)) {
iterator.remove();
}
}
}
}
if (!fields.isEmpty()) {
List list = computeFieldNames(node, fields);
// Keep error message in sync with {@link #getMissingCases}
String message = "Switch statement on an `int` with known associated constant "
+ "missing case " + Joiner.on(", ").join(list);
Location location = mContext.getNameLocation(node);
mContext.report(SWITCH_TYPE_DEF, node, location, message);
}
}
@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 PsiAnnotation node) {
PsiAnnotationMemberValue value = node.findAttributeValue(ATTR_VALUE);
if (value == null) {
value = node.findAttributeValue(null);
}
if (value == null) {
return;
}
if (!(value instanceof PsiArrayInitializerMemberValue)) {
return;
}
PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value;
PsiAnnotationMemberValue[] initializers = array.getInitializers();
Map valueToIndex =
Maps.newHashMapWithExpectedSize(initializers.length);
boolean flag = getAnnotationBooleanValue(node, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE;
if (flag) {
ensureUsingFlagStyle(initializers);
}
ConstantEvaluator constantEvaluator = new ConstantEvaluator(mContext);
for (int index = 0; index < initializers.length; index++) {
PsiAnnotationMemberValue expression = initializers[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);
PsiElement prevConstant = initializers[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.getText(), prevConstant.getText(),
repeatedValue.toString());
location = mContext.getLocation(expression);
Location secondary = mContext.getLocation(prevConstant);
secondary.setMessage("Previous same value");
location.setSecondary(secondary);
PsiElement scope = getAnnotationScope(node);
mContext.report(UNIQUE, scope, location, message);
break;
}
valueToIndex.put(number, index);
}
}
}
private void ensureUsingFlagStyle(@NonNull PsiAnnotationMemberValue[] constants) {
if (constants.length < 3) {
return;
}
for (PsiAnnotationMemberValue constant : constants) {
if (constant instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) constant).resolve();
if (resolved instanceof PsiField) {
PsiExpression initializer = ((PsiField) resolved).getInitializer();
if (initializer instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral) 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(
"Consider declaring this constant using 1 << %1$d instead",
shift);
Location location = mContext.getLocation(initializer);
mContext.report(FLAG_STYLE, initializer, location, message);
}
}
}
}
}
private boolean checkSuppressLint(@NonNull PsiAnnotation 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
PsiElement 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;
}
}
@NonNull
private static List computeFieldNames(@NonNull PsiSwitchStatement node,
Iterable> allowedValues) {
List list = Lists.newArrayList();
for (Object o : allowedValues) {
if (o instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) o).resolve();
if (resolved != null) {
o = resolved;
}
} else if (o instanceof PsiLiteral) {
list.add("`" + ((PsiLiteral) o).getValue() + '`');
continue;
}
if (o instanceof PsiField) {
PsiField field = (PsiField) o;
// Only include class name if necessary
String name = field.getName();
PsiClass clz = PsiTreeUtil.getParentOfType(node, PsiClass.class, true);
if (clz != null) {
PsiClass containingClass = field.getContainingClass();
if (containingClass != null && !containingClass.equals(clz)) {
//if (Objects.equal(containingClass.getPackage(),
// ((ResolvedClass) resolved).getPackage())) {
// name = containingClass.getSimpleName() + '.' + field.getName();
//} else {
name = containingClass.getName() + '.' + field.getName();
//}
}
}
list.add('`' + name + '`');
}
}
Collections.sort(list);
return list;
}
/**
* Given an error message produced by this lint detector for the {@link #SWITCH_TYPE_DEF} issue
* type, returns the list of missing enum cases. Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the list of enum cases, or null if not recognized
*/
@Nullable
public static List getMissingCases(@NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
String substring = findSubstring(errorMessage, " missing case ", null);
if (substring == null) {
substring = findSubstring(errorMessage, "expected one of: ", null);
}
if (substring != null) {
return Splitter.on(",").trimResults().splitToList(substring);
}
return null;
}
/**
* 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 PsiElement getAnnotationScope(@NonNull PsiAnnotation node) {
PsiElement scope = PsiTreeUtil.getParentOfType(node, PsiAnnotation.class, true);
if (scope == null) {
scope = node;
}
return scope;
}
}