@Override public Description matchClass(ClassTree classTree, VisitorState state) { TypeSymbol symbol = ASTHelpers.getSymbol(classTree); if (symbol.getKind() != ElementKind.CLASS) { return Description.NO_MATCH; } MethodTree equals = null; for (Tree member : classTree.getMembers()) { if (!(member instanceof MethodTree)) { continue; } MethodTree methodTree = (MethodTree) member; if (EQUALS_MATCHER.matches(methodTree, state)) { equals = methodTree; } } if (equals == null) { return Description.NO_MATCH; } MethodSymbol hashCodeSym = ASTHelpers.resolveExistingMethod( state, symbol, state.getName("hashCode"), ImmutableList.<Type>of(), ImmutableList.<Type>of()); if (hashCodeSym.owner.equals(state.getSymtab().objectType.tsym)) { return describeMatch(equals); } return Description.NO_MATCH; }
@Override public Description matchMethod(MethodTree methodTree, VisitorState state) { // if method is itself annotated with @Inject or it has no ancestor methods, return NO_MATCH; if (hasInjectAnnotation().matches(methodTree, state)) { return Description.NO_MATCH; } boolean foundJavaxInject = false; for (MethodSymbol superMethod : ASTHelpers.findSuperMethods(ASTHelpers.getSymbol(methodTree), state.getTypes())) { // With a Guice annotation, Guice will still inject the subclass-overridden method. if (ASTHelpers.hasAnnotation(superMethod, GUICE_INJECT_ANNOTATION, state)) { return Description.NO_MATCH; } // is not necessarily a match even if we find javax Inject on an ancestor // since a higher up ancestor may have @com.google.inject.Inject foundJavaxInject |= ASTHelpers.hasAnnotation(superMethod, JAVAX_INJECT_ANNOTATION, state); } if (foundJavaxInject) { return describeMatch( methodTree, SuggestedFix.builder() .addImport(JAVAX_INJECT_ANNOTATION) .prefixWith(methodTree, "@Inject\n") .build()); } return Description.NO_MATCH; }
@Override public boolean matches(TryTree tryTree, VisitorState state) { return ASTHelpers.findEnclosingNode(state.getPath(), DoWhileLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), EnhancedForLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), WhileLoopTree.class) != null || ASTHelpers.findEnclosingNode(state.getPath(), ForLoopTree.class) != null; }
@Override protected boolean matchArgument(ExpressionTree tree, VisitorState state) { Type type = ASTHelpers.getType(tree); if (!type.isReference()) { return false; } ClassTree classTree = ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class); if (classTree == null) { return false; } Type classType = ASTHelpers.getType(classTree); if (classType == null) { return false; } if (inEqualsOrCompareTo(classType, type, state)) { return false; } if (ASTHelpers.isSubtype(type, state.getSymtab().enumSym.type, state)) { return false; } if (ASTHelpers.isSubtype(type, state.getSymtab().classType, state)) { return false; } if (!implementsEquals(type, state)) { return false; } return true; }
/** * Fixes the error by assigning the result of the call to the receiver reference, or deleting the * method call. */ public Description describe(MethodInvocationTree methodInvocationTree, VisitorState state) { // Find the root of the field access chain, i.e. a.intern().trim() ==> a. ExpressionTree identifierExpr = ASTHelpers.getRootAssignable(methodInvocationTree); String identifierStr = null; Type identifierType = null; if (identifierExpr != null) { identifierStr = identifierExpr.toString(); if (identifierExpr instanceof JCIdent) { identifierType = ((JCIdent) identifierExpr).sym.type; } else if (identifierExpr instanceof JCFieldAccess) { identifierType = ((JCFieldAccess) identifierExpr).sym.type; } else { throw new IllegalStateException("Expected a JCIdent or a JCFieldAccess"); } } Type returnType = ASTHelpers.getReturnType(((JCMethodInvocation) methodInvocationTree).getMethodSelect()); Fix fix; if (identifierStr != null && !"this".equals(identifierStr) && returnType != null && state.getTypes().isAssignable(returnType, identifierType)) { // Fix by assigning the assigning the result of the call to the root receiver reference. fix = SuggestedFix.prefixWith(methodInvocationTree, identifierStr + " = "); } else { // Unclear what the programmer intended. Delete since we don't know what else to do. Tree parent = state.getPath().getParentPath().getLeaf(); fix = SuggestedFix.delete(parent); } return describeMatch(methodInvocationTree, fix); }
@Override public Description matchMethodInvocation( MethodInvocationTree methodInvocationTree, VisitorState state) { if (methodInvocationTree.getArguments().isEmpty()) { return Description.NO_MATCH; } if (!TRUTH_SUBJECT_CALL.matches(methodInvocationTree, state)) { return Description.NO_MATCH; } ExpressionTree rec = ASTHelpers.getReceiver(methodInvocationTree); if (rec == null) { return Description.NO_MATCH; } if (!ASSERT_THAT.matches(rec, state)) { return Description.NO_MATCH; } ExpressionTree expr = getOnlyElement(((MethodInvocationTree) rec).getArguments()); if (expr == null) { return Description.NO_MATCH; } // check that argument of assertThat is a constant if (ASTHelpers.constValue(expr) == null) { return Description.NO_MATCH; } // check that expectation isn't a constant ExpressionTree expectation = getOnlyElement(methodInvocationTree.getArguments()); if (ASTHelpers.constValue(expectation) != null) { return Description.NO_MATCH; } SuggestedFix fix = SuggestedFix.swap(expr, expectation); return buildDescription(methodInvocationTree).addFix(fix).build(); }
@Override public Optional<MatchState> matchResult(ExpressionTree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); if (!(sym instanceof MethodSymbol)) { return Optional.absent(); } if (tree instanceof NewClassTree) { // Don't match constructors as they are neither static nor instance methods. return Optional.absent(); } if (tree instanceof MethodInvocationTree) { tree = ((MethodInvocationTree) tree).getMethodSelect(); } return Optional.of( MatchState.create(ASTHelpers.getReceiverType(tree), (MethodSymbol) sym)); }
@Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(expressionTree); if (sym == null) { return false; } if (!(sym instanceof MethodSymbol)) { throw new IllegalArgumentException( "DescendantOf matcher expects a method call but found " + sym.getClass() + ". Expression: " + expressionTree); } if (sym.isStatic()) { return false; } if (methodName.equals(sym.toString())) { Type accessedReferenceType = sym.owner.type; Type collectionType = state.getTypeFromString(fullClassName); if (collectionType != null) { return state .getTypes() .isSubtype(accessedReferenceType, state.getTypes().erasure(collectionType)); } } return false; }
// TODO(eaftan): refactor other code that accesses symbols to use this method public static Symbol getSymbol(Tree tree) { if (tree instanceof ClassTree) { return getSymbol((ClassTree) tree); } if (tree instanceof MethodTree) { return getSymbol((MethodTree) tree); } if (tree instanceof VariableTree) { return getSymbol((VariableTree) tree); } if (tree instanceof JCFieldAccess) { return ((JCFieldAccess) tree).sym; } if (tree instanceof JCIdent) { return ((JCIdent) tree).sym; } if (tree instanceof JCMethodInvocation) { return ASTHelpers.getSymbol((MethodInvocationTree) tree); } if (tree instanceof JCNewClass) { return ((JCNewClass) tree).constructor; } if (tree instanceof AnnotationTree) { return getSymbol(((AnnotationTree) tree).getAnnotationType()); } if (tree instanceof PackageTree) { return getSymbol((PackageTree) tree); } return null; }
/** * Extend suppression sets for both {@code @SuppressWarnings} and custom suppression annotations. * When we explore a new node, we have to extend the suppression sets with any new suppressed * warnings or custom suppression annotations. We also have to retain the previous suppression set * so that we can reinstate it when we move up the tree. * * <p>We do not modify the existing suppression sets, so they can be restored when moving up the * tree. We also avoid copying the suppression sets if the next node to explore does not have any * suppressed warnings or custom suppression annotations. This is the common case. * * @param sym The {@code Symbol} for the AST node currently being scanned * @param suppressWarningsType The {@code Type} for {@code @SuppressWarnings}, as given by javac's * symbol table * @param suppressionsOnCurrentPath The set of strings in all {@code @SuppressWarnings} * annotations on the current path through the AST * @param customSuppressionsOnCurrentPath The set of all custom suppression annotations */ public NewSuppressions extendSuppressionSets( Symbol sym, Type suppressWarningsType, Set<String> suppressionsOnCurrentPath, Set<Class<? extends Annotation>> customSuppressionsOnCurrentPath, boolean inGeneratedCode) { boolean newInGeneratedCode = inGeneratedCode || ASTHelpers.hasAnnotation(sym, Generated.class); /** Handle custom suppression annotations. */ Set<Class<? extends Annotation>> newCustomSuppressions = null; for (Class<? extends Annotation> annotationType : customSuppressionAnnotations) { if (ASTHelpers.hasAnnotation(sym, annotationType)) { if (newCustomSuppressions == null) { newCustomSuppressions = new HashSet<>(customSuppressionsOnCurrentPath); } newCustomSuppressions.add(annotationType); } } /** Handle @SuppressWarnings. */ Set<String> newSuppressions = null; // Iterate over annotations on this symbol, looking for SuppressWarnings for (Attribute.Compound attr : sym.getAnnotationMirrors()) { // TODO(eaftan): use JavacElements.getAnnotation instead if (attr.type.tsym == suppressWarningsType.tsym) { for (List<Pair<MethodSymbol, Attribute>> v = attr.values; v.nonEmpty(); v = v.tail) { Pair<MethodSymbol, Attribute> value = v.head; if (value.fst.name.toString().equals("value")) if (value.snd instanceof Attribute.Array) { // SuppressWarnings takes an array for (Attribute suppress : ((Attribute.Array) value.snd).values) { if (newSuppressions == null) { newSuppressions = new HashSet<>(suppressionsOnCurrentPath); } // TODO(eaftan): check return value to see if this was a new warning? newSuppressions.add((String) suppress.getValue()); } } else { throw new RuntimeException("Expected SuppressWarnings annotation to take array type"); } } } } return new NewSuppressions(newSuppressions, newCustomSuppressions, newInGeneratedCode); }
/** Gets the symbol for a method invocation. */ public static MethodSymbol getSymbol(MethodInvocationTree tree) { Symbol sym = ASTHelpers.getSymbol(tree.getMethodSelect()); if (!(sym instanceof MethodSymbol)) { // Defensive. Would only occur if there are errors in the AST. return null; } return (MethodSymbol) sym; }
private boolean inEqualsOrCompareTo(Type classType, Type type, VisitorState state) { MethodTree methodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class); if (methodTree == null) { return false; } MethodSymbol sym = ASTHelpers.getSymbol(methodTree); if (sym == null || sym.isStatic()) { return false; } Symbol compareTo = getOnlyMember(state, state.getSymtab().comparableType, "compareTo"); Symbol equals = getOnlyMember(state, state.getSymtab().objectType, "equals"); if (!sym.overrides(compareTo, classType.tsym, state.getTypes(), false) && !sym.overrides(equals, classType.tsym, state.getTypes(), false)) { return false; } if (!ASTHelpers.isSameType(type, classType, state)) { return false; } return true; }
@Override public boolean matches(TryTree tryTree, VisitorState state) { MethodTree enclosingMethodTree = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class); Name name = enclosingMethodTree.getName(); return JUnitMatchers.looksLikeJUnit3SetUp.matches(enclosingMethodTree, state) || JUnitMatchers.looksLikeJUnit3TearDown.matches(enclosingMethodTree, state) || name.contentEquals("main") // TODO(schmitt): Move to JUnitMatchers? || name.contentEquals("suite") || Matchers.hasAnnotation(JUNIT_BEFORE_ANNOTATION).matches(enclosingMethodTree, state) || Matchers.hasAnnotation(JUNIT_AFTER_ANNOTATION).matches(enclosingMethodTree, state); }
@Override @Nullable protected Unifier defaultAction(Tree node, @Nullable Unifier unifier) { Symbol symbol = ASTHelpers.getSymbol(node); if (symbol != null && symbol.getEnclosingElement() != null && symbol .getEnclosingElement() .getQualifiedName() .contentEquals(classIdent().getQualifiedName()) && symbol.getSimpleName().contentEquals(member())) { return memberType().unify(symbol.asType(), unifier); } return null; }
@Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { if (tree.getTypeDecls().size() <= 1) { // package-info.java files have zero top-level declarations, everything // else should have exactly one. return Description.NO_MATCH; } if (tree.getPackageName() == null) { // Real code doesn't use the default package. return Description.NO_MATCH; } List<String> names = new ArrayList<>(); for (Tree member : tree.getTypeDecls()) { if (member instanceof ClassTree) { ClassTree classMember = (ClassTree) member; switch (classMember.getKind()) { case CLASS: case INTERFACE: case ANNOTATION_TYPE: case ENUM: SuppressWarnings suppression = ASTHelpers.getAnnotation(classMember, SuppressWarnings.class); if (suppression != null && !Collections.disjoint(Arrays.asList(suppression.value()), allNames())) { // If any top-level classes have @SuppressWarnings("TopLevel"), ignore // this compilation unit. We can't rely on the normal suppression // mechanism because the only enclosing element is the package declaration, // and @SuppressWarnings can't be applied to packages. return Description.NO_MATCH; } names.add(classMember.getSimpleName().toString()); break; default: break; } } } if (names.size() <= 1) { // this can happen with multiple type declarations if some of them are // empty (e.g. ";" at the top level counts as an empty type decl) return Description.NO_MATCH; } String message = String.format( "Expected at most one top-level class declaration, instead found: %s", Joiner.on(", ").join(names)); return buildDescription(tree.getPackageName()).setMessage(message).build(); }
@Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(expressionTree); if (sym != null && sym.getSimpleName().toString().startsWith("log")) { return true; } if (sym != null && sym.isStatic()) { if (sym.owner.getQualifiedName().toString().contains("Logger")) { return true; } } else if (expressionTree instanceof MemberSelectTree) { if (((MemberSelectTree) expressionTree).getExpression().toString().startsWith("log")) { return true; } } return false; }
/** Check if the method declares or inherits an implementation of .equals() */ public static boolean implementsEquals(Type type, VisitorState state) { Name equalsName = state.getName("equals"); Symbol objectEquals = getOnlyMember(state, state.getSymtab().objectType, "equals"); for (Type sup : state.getTypes().closure(type)) { if (sup.tsym.isInterface()) { continue; } if (ASTHelpers.isSameType(sup, state.getSymtab().objectType, state)) { return false; } WriteableScope scope = sup.tsym.members(); if (scope == null) { continue; } for (Symbol sym : scope.getSymbolsByName(equalsName)) { if (sym.overrides(objectEquals, type.tsym, state.getTypes(), false)) { return true; } } } return false; }
@Override public boolean matches(ExpressionTree expressionTree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(expressionTree); return sym != null && sym.getSimpleName().toString().startsWith("assert"); }