/* * 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.detector.api.JavaContext.findSurroundingClass; import static com.android.tools.lint.detector.api.JavaContext.getParentOfType; 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.ResolvedNode; 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.google.common.collect.Lists; import com.google.common.collect.Maps; import java.io.File; import java.util.Collections; import java.util.List; import java.util.Map; 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.Block; import lombok.ast.ClassDeclaration; import lombok.ast.ConstructorDeclaration; import lombok.ast.Expression; import lombok.ast.ForwardingAstVisitor; import lombok.ast.IntegralLiteral; import lombok.ast.MethodDeclaration; import lombok.ast.Modifiers; import lombok.ast.Node; import lombok.ast.Select; import lombok.ast.StrictListAccessor; import lombok.ast.StringLiteral; 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); /** 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<Class<? extends Node>> getApplicableNodeTypes() { return Collections.<Class<? extends Node>>singletonList(Annotation.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<Expression, ArrayInitializer> 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); } private void ensureUniqueValues(@NonNull ResolvedAnnotation annotation, @NonNull Annotation node) { Object allowed = annotation.getValue(); if (allowed instanceof Object[]) { Object[] allowedValues = (Object[]) allowed; Map<Number,Integer> valueToIndex = Maps.newHashMapWithExpectedSize(allowedValues.length); List<Node> 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<VariableDefinitionEntry> findDeclarations( @Nullable ClassDeclaration cls, @NonNull List<VariableReference> references) { if (cls == null) { return Collections.emptyList(); } Map<String, VariableReference> referenceMap = Maps.newHashMap(); for (VariableReference reference : references) { String name = reference.astIdentifier().astValue(); referenceMap.put(name, reference); } List<VariableDefinitionEntry> 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<Node> constants) { if (constants.size() < 3) { return; } List<VariableReference> references = Lists.newArrayListWithExpectedSize(constants.size()); for (Node constant : constants) { if (constant instanceof VariableReference) { references.add((VariableReference) constant); } } List<VariableDefinitionEntry> 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; } } /** * 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; } }