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.SUPPRESS_LINT;
import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE;
import static com.android.tools.lint.checks.SupportAnnotationDetector.filterRelevantAnnotations;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.lint.detector.api.JavaContext.findSurroundingClass;
import static com.android.tools.lint.detector.api.JavaContext.getParentOfType;
import static com.android.tools.lint.detector.api.LintUtils.findSubstring;
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.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedField;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
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.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
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 java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import lombok.ast.Annotation;
import lombok.ast.AnnotationDeclaration;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationValue;
import lombok.ast.ArrayInitializer;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.Block;
import lombok.ast.Case;
import lombok.ast.Cast;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Modifiers;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.Statement;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
import lombok.ast.Switch;
import lombok.ast.TypeBody;
import lombok.ast.TypeMember;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Checks annotations to make sure they are valid
*/
public class AnnotationDetector extends Detector implements Detector.JavaScanner {
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);
/** 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() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List> getApplicableNodeTypes() {
//noinspection unchecked
return Arrays.>asList(Annotation.class, Switch.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private static class AnnotationChecker extends ForwardingAstVisitor {
private final JavaContext mContext;
public AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitAnnotation(Annotation node) {
String type = node.astAnnotationTypeReference().getTypeName();
if (SUPPRESS_LINT.equals(type) || FQCN_SUPPRESS_LINT.equals(type)) {
Node parent = node.getParent();
if (parent instanceof Modifiers) {
parent = parent.getParent();
if (parent instanceof VariableDefinition) {
for (AnnotationElement element : node.astElements()) {
AnnotationValue valueNode = element.astValue();
if (valueNode == null) {
continue;
}
if (valueNode instanceof StringLiteral) {
StringLiteral literal = (StringLiteral) valueNode;
String id = literal.astValue();
if (!checkId(node, id)) {
return super.visitAnnotation(node);
}
} else if (valueNode instanceof ArrayInitializer) {
ArrayInitializer array = (ArrayInitializer) valueNode;
StrictListAccessor expressions =
array.astExpressions();
if (expressions == null) {
continue;
}
for (Expression arrayElement : expressions) {
if (arrayElement instanceof StringLiteral) {
String id = ((StringLiteral) arrayElement).astValue();
if (!checkId(node, id)) {
return super.visitAnnotation(node);
}
}
}
}
}
}
}
} else if (INT_DEF_ANNOTATION.equals(type) || "IntDef".equals(type)) {
// Make sure that all the constants are unique
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedAnnotation) {
ensureUniqueValues(((ResolvedAnnotation)resolved), node);
}
}
return super.visitAnnotation(node);
}
@Override
public boolean visitSwitch(Switch node) {
Expression condition = node.astCondition();
TypeDescriptor type = mContext.getType(condition);
if (type != null && type.matchesName(TYPE_INT)) {
ResolvedAnnotation annotation = findIntDef(condition);
if (annotation != null) {
checkSwitch(node, annotation);
}
}
return super.visitSwitch(node);
}
/**
* Searches for the corresponding @IntDef annotation definition associated
* with a given node
*/
@Nullable
private ResolvedAnnotation findIntDef(@NonNull Node node) {
if ((node instanceof VariableReference || node instanceof Select)) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved == null) {
return null;
}
ResolvedAnnotation annotation = SupportAnnotationDetector.findIntDef(
filterRelevantAnnotations(resolved.getAnnotations()));
if (annotation != null) {
return annotation;
}
if (node instanceof VariableReference) {
Statement statement = getParentOfType(node, Statement.class, false);
if (statement != null) {
ListIterator iterator =
statement.getParent().getChildren().listIterator();
while (iterator.hasNext()) {
if (iterator.next() == statement) {
if (iterator.hasPrevious()) { // should always be true
iterator.previous();
}
break;
}
}
String targetName = ((VariableReference) node).astIdentifier().astValue();
while (iterator.hasPrevious()) {
Node previous = iterator.previous();
if (previous instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration) previous;
VariableDefinition definition = declaration.astDefinition();
for (VariableDefinitionEntry entry : definition
.astVariables()) {
if (entry.astInitializer() != null
&& entry.astName().astValue().equals(targetName)) {
return findIntDef(entry.astInitializer());
}
}
} else if (previous instanceof ExpressionStatement) {
ExpressionStatement expressionStatement =
(ExpressionStatement) previous;
Expression expression = expressionStatement.astExpression();
if (expression instanceof BinaryExpression &&
((BinaryExpression) expression).astOperator()
== BinaryOperator.ASSIGN) {
BinaryExpression binaryExpression
= (BinaryExpression) expression;
if (targetName.equals(binaryExpression.astLeft().toString())) {
return findIntDef(binaryExpression.astRight());
}
}
}
}
}
}
} else if (node instanceof MethodInvocation) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved != null) {
ResolvedAnnotation annotation = SupportAnnotationDetector
.findIntDef(filterRelevantAnnotations(resolved.getAnnotations()));
if (annotation != null) {
return annotation;
}
}
} else if (node instanceof InlineIfExpression) {
InlineIfExpression expression = (InlineIfExpression) node;
if (expression.astIfTrue() != null) {
ResolvedAnnotation result = findIntDef(expression.astIfTrue());
if (result != null) {
return result;
}
}
if (expression.astIfFalse() != null) {
ResolvedAnnotation result = findIntDef(expression.astIfFalse());
if (result != null) {
return result;
}
}
} else if (node instanceof Cast) {
Cast cast = (Cast) node;
return findIntDef(cast.astOperand());
}
return null;
}
private void checkSwitch(@NonNull Switch node, @NonNull ResolvedAnnotation annotation) {
Block block = node.astBody();
if (block == null) {
return;
}
Object allowed = annotation.getValue();
if (!(allowed instanceof Object[])) {
return;
}
Object[] allowedValues = (Object[]) allowed;
List fields = Lists.newArrayListWithCapacity(allowedValues.length);
for (Object o : allowedValues) {
if (o instanceof ResolvedField) {
fields.add((ResolvedField) o);
}
}
// 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.astContents().isEmpty()) { return; }
for (Statement statement : block.astContents()) {
if (statement instanceof Case) {
Case caseStatement = (Case) statement;
Expression expression = caseStatement.astCondition();
if (expression instanceof IntegralLiteral) {
// 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 != null) { // default case can have null expression
ResolvedNode resolved = mContext.resolve(expression);
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 ResolvedField) {
// We can't just do
// fields.remove(resolved);
// since the fields list contains instances of potentially
// different types with different hash codes. The
// equals method on ResolvedExternalField 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.
ListIterator iterator = fields.listIterator();
while (iterator.hasNext()) {
ResolvedField field = iterator.next();
if (field.equals(resolved)) {
iterator.remove();
break;
}
}
}
}
}
}
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);
}
}
private void ensureUniqueValues(@NonNull ResolvedAnnotation annotation,
@NonNull Annotation node) {
Object allowed = annotation.getValue();
if (allowed instanceof Object[]) {
Object[] allowedValues = (Object[]) allowed;
Map valueToIndex =
Maps.newHashMapWithExpectedSize(allowedValues.length);
List constants = null;
for (AnnotationElement element : node.astElements()) {
if (element.astName() == null
|| ATTR_VALUE.equals(element.astName().astValue())) {
AnnotationValue value = element.astValue();
if (value instanceof ArrayInitializer) {
ArrayInitializer initializer = (ArrayInitializer)value;
constants = Lists.newArrayListWithExpectedSize(allowedValues.length);
for (Expression expression : initializer.astExpressions()) {
constants.add(expression);
}
}
break;
}
}
if (constants != null) {
if (constants.size() != allowedValues.length) {
constants = null;
} else {
boolean flag = annotation.getValue(TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE;
if (flag) {
ensureUsingFlagStyle(constants);
}
}
}
for (int index = 0; index < allowedValues.length; index++) {
Object o = allowedValues[index];
if (o instanceof Number) {
Number number = (Number)o;
if (valueToIndex.containsKey(number)) {
@SuppressWarnings("UnnecessaryLocalVariable")
Number repeatedValue = number;
Location location;
String message;
if (constants != null) {
Node constant = constants.get(index);
int prevIndex = valueToIndex.get(number);
Node prevConstant = constants.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",
constant.toString(), prevConstant.toString(),
repeatedValue.toString());
location = mContext.getLocation(constant);
Location secondary = mContext.getLocation(prevConstant);
secondary.setMessage("Previous same value");
location.setSecondary(secondary);
} else {
message = String.format(
"More than one constant specifies the same exact "
+ "value (%1$s); this is usually a cut & paste or"
+ "merge error",
repeatedValue.toString());
location = mContext.getLocation(node);
}
Node scope = getAnnotationScope(node);
mContext.report(UNIQUE, scope, location, message);
break;
}
valueToIndex.put(number, index);
}
}
}
}
@NonNull
private static List findDeclarations(
@Nullable ClassDeclaration cls,
@NonNull List references) {
if (cls == null) {
return Collections.emptyList();
}
Map referenceMap = Maps.newHashMap();
for (VariableReference reference : references) {
String name = reference.astIdentifier().astValue();
referenceMap.put(name, reference);
}
List declarations = Lists.newArrayList();
for (TypeMember member : cls.astBody().astMembers()) {
if (member instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration)member;
VariableDefinitionEntry field = declaration.astDefinition().astVariables()
.first();
String name = field.astName().astValue();
if (referenceMap.containsKey(name)) {
// TODO: When the Lombok ECJ bridge properly handles resolving variable
// definitions into ECJ bindings this code should check that
// mContext.resolve(field) == mContext.resolve(referenceMap.get(name)) !
declarations.add(field);
}
}
}
return declarations;
}
private void ensureUsingFlagStyle(@NonNull List constants) {
if (constants.size() < 3) {
return;
}
List references =
Lists.newArrayListWithExpectedSize(constants.size());
for (Node constant : constants) {
if (constant instanceof VariableReference) {
references.add((VariableReference) constant);
}
}
List entries = findDeclarations(
findSurroundingClass(constants.get(0)), references);
for (VariableDefinitionEntry entry : entries) {
Expression declaration = entry.astInitializer();
if (declaration == null) {
continue;
}
if (declaration instanceof IntegralLiteral) {
IntegralLiteral literal = (IntegralLiteral) declaration;
// Allow -1, 0 and 1. You can write 1 as "1 << 0" but IntelliJ for
// example warns that that's a redundant shift.
long value = literal.astLongValue();
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);
String message = String.format(
"Consider declaring this constant using 1 << %1$d instead",
shift);
mContext.report(FLAG_STYLE, declaration, mContext.getLocation(declaration),
message);
}
}
}
private boolean checkId(Annotation node, 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) {
// Ensure that this isn't a field
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof MethodDeclaration
|| parent instanceof ConstructorDeclaration
|| parent instanceof Block) {
break;
} else if (parent instanceof TypeBody) { // It's a field
return true;
} else if (issue == ApiDetector.UNSUPPORTED
&& parent instanceof VariableDefinition) {
VariableDefinition definition = (VariableDefinition) parent;
for (VariableDefinitionEntry entry : definition.astVariables()) {
Expression initializer = entry.astInitializer();
if (initializer instanceof Select) {
return true;
}
}
}
parent = parent.getParent();
if (parent == null) {
return true;
}
}
// This issue doesn't have AST access: annotations are not
// available for local variables or parameters
Node 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 List computeFieldNames(@NonNull Switch node, Iterable allowedValues) {
List list = Lists.newArrayList();
for (Object o : allowedValues) {
if (o instanceof ResolvedField) {
ResolvedField field = (ResolvedField) o;
// Only include class name if necessary
String name = field.getName();
ClassDeclaration clz = findSurroundingClass(node);
if (clz != null) {
ResolvedNode resolved = mContext.resolve(clz);
ResolvedClass containingClass = field.getContainingClass();
if (containingClass != null && !containingClass.equals(resolved)
&& resolved instanceof ResolvedClass) {
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 Node getAnnotationScope(@NonNull Annotation node) {
Node scope = getParentOfType(node,
AnnotationDeclaration.class, true);
if (scope == null) {
scope = node;
}
return scope;
}
}