/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.errorprone.bugpatterns.collectionincompatibletype; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.errorprone.BugPattern.Category.JDK; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.CompatibleWith; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.Matchers; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; import java.util.ArrayList; import java.util.List; import java.util.Set; /** @author glorioso@google.com (Nick Glorioso) */ @BugPattern( name = "CompatibleWithAnnotationMisuse", summary = "@CompatibleWith's value is not a type argument.", explanation = "The `@CompatibleWith` annotation is used to mark parameters that need extra " + "type checking on arguments passed to the method. The annotation was not appropriately " + "placed on a parameter with a valid type argument. See the javadoc for more details.", severity = ERROR, category = JDK ) public class CompatibleWithMisuse extends BugChecker implements AnnotationTreeMatcher { private static final Matcher<AnnotationTree> IS_COMPATIBLE_WITH_ANNOTATION = Matchers.isType(CompatibleWith.class.getCanonicalName()); @Override public Description matchAnnotation(AnnotationTree annoTree, VisitorState state) { if (!IS_COMPATIBLE_WITH_ANNOTATION.matches(annoTree, state)) { return Description.NO_MATCH; } // Hunt for type args on the declared method // TODO(glorioso): Once annotation is TYPE_USE, make sure that the node is actually a method // parameter MethodTree methodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class); VariableTree paramTree = ASTHelpers.findEnclosingNode(state.getPath(), VariableTree.class); MethodSymbol declaredMethod = ASTHelpers.getSymbol(methodTree); // We're disallowing tags on varargs methods for now, but we may revisit it in the future. if (declaredMethod.isVarArgs() && Iterables.getLast(methodTree.getParameters()) == paramTree) { return describeWithMessage(annoTree, "@CompatibleWith can't be used on a varargs parameter"); } // If this method overrides other methods, ensure that none of them have @CompatibleWith. // This restriction may need to be removed to allow more complex declaration hierarchies. for (MethodSymbol methodSymbol : ASTHelpers.findSuperMethods(declaredMethod, state.getTypes())) { if (methodSymbol .params() .stream() .anyMatch(p -> ASTHelpers.hasAnnotation(p, CompatibleWith.class, state))) { return describeWithMessage( annoTree, String.format( "This method overrides a method in %s that already has @CompatibleWith", methodSymbol.owner.getSimpleName())); } } List<TypeVariableSymbol> potentialTypeVars = new ArrayList<>(declaredMethod.getTypeParameters()); // Check enclosing types (not superclasses) ClassSymbol cs = (ClassSymbol) declaredMethod.owner; do { potentialTypeVars.addAll(cs.getTypeParameters()); cs = cs.isInner() ? cs.owner.enclClass() : null; } while (cs != null); if (potentialTypeVars.isEmpty()) { return describeWithMessage( annoTree, "There are no type arguments in scope to match against."); } Set<String> validNames = potentialTypeVars .stream() .map(TypeVariableSymbol::getSimpleName) .map(Object::toString) .collect(toImmutableSet()); String constValue = valueArgumentFromCompatibleWithAnnotation(annoTree); if (constValue == null || constValue.isEmpty()) { return describeWithMessage( annoTree, String.format( "The value of @CompatibleWith must not be empty (valid arguments are %s)", printTypeArgs(validNames))); } return validNames.contains(constValue) ? Description.NO_MATCH : describeWithMessage( annoTree, String.format( "%s is not a valid type argument. Valid arguments are: %s", constValue, printTypeArgs(validNames))); } // @CompatibleWith("X"), @CompatibleWith(value = "X"), // @CompatibleWith(SOME_FIELD_WHOSE_CONSTANT_VALUE_IS_X) // => X // This function assumes the the annotation tree will only have one argument, of type String, that // is required. private String valueArgumentFromCompatibleWithAnnotation(AnnotationTree tree) { ExpressionTree argumentValue = Iterables.getOnlyElement(tree.getArguments()); if (argumentValue.getKind() != Kind.ASSIGNMENT) { // :-| Annotation symbol broken. Punt? return null; } return ASTHelpers.constValue(((AssignmentTree) argumentValue).getExpression(), String.class); } private String printTypeArgs(Set<String> validNames) { return Joiner.on(", ").join(validNames); } private Description describeWithMessage(Tree tree, String message) { return buildDescription(tree).setMessage(message).build(); } }