/* * Copyright 2015 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; import static com.google.errorprone.BugPattern.Category.GUAVA; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.Matchers.argument; import static com.google.errorprone.matchers.Matchers.classLiteral; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.isSameType; import static com.google.errorprone.util.ASTHelpers.isSubtype; import static com.sun.tools.javac.code.TypeTag.BOT; import static javax.lang.model.element.Modifier.PUBLIC; import com.google.common.util.concurrent.Futures; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.fixes.Fix; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Types; /** * Checks for calls to Guava's {@code Futures.getChecked} method that will always fail because they * pass an incompatible exception type. */ @BugPattern( name = "FuturesGetCheckedIllegalExceptionType", summary = "Futures.getChecked requires a checked exception type with a standard constructor.", explanation = "The passed exception type must not be a RuntimeException, and it must expose a " + "public constructor whose only parameters are of type String or Throwable. getChecked " + "will reject any other type with an IllegalArgumentException.", category = GUAVA, severity = ERROR ) public final class FuturesGetCheckedIllegalExceptionType extends BugChecker implements MethodInvocationTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { if (!FUTURES_GET_CHECKED_MATCHER.matches(tree, state)) { return NO_MATCH; } /* * Check for RuntimeException first: There would be no sense in telling the user that the * problem is related to which constructors are available if we'd reject the call anyway. */ if (PASSED_RUNTIME_EXCEPTION_TYPE.matches(tree, state)) { return describeUncheckedExceptionTypeMatch( tree, SuggestedFix.builder() .replace(tree, "getUnchecked(" + tree.getArguments().get(0) + ")") .addStaticImport(Futures.class.getName() + ".getUnchecked") .build()); } if (PASSED_TYPE_WITHOUT_USABLE_CONSTRUCTOR.matches(tree, state)) { return describeNoValidConstructorMatch(tree); } return NO_MATCH; } private static final Matcher<ExpressionTree> FUTURES_GET_CHECKED_MATCHER = anyOf( staticMethod().onClass( Futures.class.getName()).named("getChecked")); private static final Matcher<ExpressionTree> CLASS_OBJECT_FOR_CLASS_EXTENDING_RUNTIME_EXCEPTION = new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree tree, VisitorState state) { Types types = state.getTypes(); Type classType = state.getSymtab().classType; Type runtimeExceptionType = state.getSymtab().runtimeExceptionType; Type argType = getType(tree); // Make sure that the argument is a Class<Something> (and not null/bottom). if (!isSubtype(argType, classType, state) || argType.getTag() == BOT) { return false; } Type exceptionType = ((ClassType) argType).getTypeArguments().head; return types.isSubtype(exceptionType, runtimeExceptionType); } }; private static final Matcher<MethodInvocationTree> PASSED_RUNTIME_EXCEPTION_TYPE = argument(1, CLASS_OBJECT_FOR_CLASS_EXTENDING_RUNTIME_EXCEPTION); private static final Matcher<ExpressionTree> CLASS_OBJECT_FOR_CLASS_WITHOUT_USABLE_CONSTRUCTOR = classLiteral(new Matcher<ExpressionTree>() { @Override public boolean matches(ExpressionTree tree, VisitorState state) { ClassSymbol classSymbol = (ClassSymbol) getSymbol(tree); if (classSymbol == null) { return false; } if (classSymbol.isInner()) { return true; } for (Symbol enclosedSymbol : classSymbol.getEnclosedElements()) { if (!enclosedSymbol.isConstructor()) { continue; } MethodSymbol constructorSymbol = (MethodSymbol) enclosedSymbol; if (canBeUsedByGetChecked(constructorSymbol, state)) { return false; } } return true; } }); private static final Matcher<MethodInvocationTree> PASSED_TYPE_WITHOUT_USABLE_CONSTRUCTOR = argument(1, CLASS_OBJECT_FOR_CLASS_WITHOUT_USABLE_CONSTRUCTOR); private static boolean canBeUsedByGetChecked(MethodSymbol constructor, VisitorState state) { Type stringType = state.getSymtab().stringType; Type throwableType = state.getSymtab().throwableType; // TODO(cpovirk): Check visibility of enclosing types (assuming that it matters to getChecked). if (!constructor.getModifiers().contains(PUBLIC)) { return false; } for (VarSymbol param : constructor.getParameters()) { if (!isSameType(param.asType(), stringType, state) && !isSameType(param.asType(), throwableType, state)) { return false; } } return true; } private Description describeUncheckedExceptionTypeMatch(Tree tree, Fix fix) { return buildDescription(tree) .setMessage("The exception class passed to getChecked must be a checked exception, " + "not a RuntimeException.") .addFix(fix) .build(); } private Description describeNoValidConstructorMatch(Tree tree) { return buildDescription(tree) .setMessage( "The exception class passed to getChecked must declare a public constructor whose " + "only parameters are of type String or Throwable.") .build(); } }