Skip to content

Commit a66e856

Browse files
cpovirkError Prone Team
authored andcommitted
Suggest more possible fixes for CheckReturnValue errors.
PiperOrigin-RevId: 478925901
1 parent 266a16e commit a66e856

File tree

6 files changed

+463
-3
lines changed

6 files changed

+463
-3
lines changed

core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@
1919
import static com.google.common.base.Preconditions.checkArgument;
2020
import static com.google.common.base.Suppliers.memoize;
2121
import static com.google.common.collect.ImmutableList.toImmutableList;
22+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
23+
import static com.google.common.collect.Lists.reverse;
2224
import static com.google.common.collect.Multimaps.toMultimap;
2325
import static com.google.errorprone.fixes.SuggestedFix.delete;
26+
import static com.google.errorprone.fixes.SuggestedFix.postfixWith;
2427
import static com.google.errorprone.fixes.SuggestedFix.prefixWith;
28+
import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport;
2529
import static com.google.errorprone.matchers.Description.NO_MATCH;
2630
import static com.google.errorprone.matchers.Matchers.allOf;
2731
import static com.google.errorprone.matchers.Matchers.isThrowingFunctionalInterface;
2832
import static com.google.errorprone.matchers.Matchers.not;
2933
import static com.google.errorprone.matchers.Matchers.parentNode;
34+
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
3035
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
3136
import static com.google.errorprone.util.ASTHelpers.getResultType;
3237
import static com.google.errorprone.util.ASTHelpers.getRootAssignable;
@@ -36,10 +41,16 @@
3641
import static com.google.errorprone.util.ASTHelpers.getUpperBound;
3742
import static com.google.errorprone.util.ASTHelpers.isSubtype;
3843
import static com.google.errorprone.util.ASTHelpers.isVoidType;
44+
import static com.google.errorprone.util.ASTHelpers.matchingMethods;
45+
import static com.google.errorprone.util.FindIdentifiers.findAllIdents;
3946
import static com.sun.source.tree.Tree.Kind.EXPRESSION_STATEMENT;
47+
import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;
4048
import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
4149
import static com.sun.source.tree.Tree.Kind.NEW_CLASS;
50+
import static com.sun.tools.javac.parser.Tokens.TokenKind.RPAREN;
4251
import static java.lang.String.format;
52+
import static java.util.stream.IntStream.range;
53+
import static java.util.stream.Stream.concat;
4354

4455
import com.google.common.collect.ImmutableList;
4556
import com.google.common.collect.ImmutableListMultimap;
@@ -63,6 +74,7 @@
6374
import com.sun.source.tree.LambdaExpressionTree;
6475
import com.sun.source.tree.MemberReferenceTree;
6576
import com.sun.source.tree.MemberReferenceTree.ReferenceMode;
77+
import com.sun.source.tree.MemberSelectTree;
6678
import com.sun.source.tree.MethodInvocationTree;
6779
import com.sun.source.tree.MethodTree;
6880
import com.sun.source.tree.NewClassTree;
@@ -81,6 +93,7 @@
8193
import java.util.Queue;
8294
import java.util.Set;
8395
import java.util.function.Supplier;
96+
import java.util.stream.Stream;
8497
import javax.lang.model.element.Name;
8598
import javax.lang.model.type.TypeKind;
8699

@@ -256,6 +269,64 @@ final ImmutableList<Fix> fixesAtCallSite(ExpressionTree invocationTree, VisitorS
256269
* Luckily, they're not a ton harder to include than plain code comments would be.
257270
*/
258271
ImmutableMap.Builder<String, SuggestedFix> fixes = ImmutableMap.builder();
272+
if (MOCKITO_VERIFY.matches(invocationTree, state)) {
273+
ExpressionTree maybeCallToMock =
274+
((MethodInvocationTree) invocationTree).getArguments().get(0);
275+
if (maybeCallToMock.getKind() == METHOD_INVOCATION) {
276+
ExpressionTree maybeMethodSelectOnMock =
277+
((MethodInvocationTree) maybeCallToMock).getMethodSelect();
278+
if (maybeMethodSelectOnMock.getKind() == MEMBER_SELECT) {
279+
MemberSelectTree maybeSelectOnMock = (MemberSelectTree) maybeMethodSelectOnMock;
280+
// For this suggestion, we want to move the closing parenthesis:
281+
// verify(foo .bar())
282+
// ^ v
283+
// +------+
284+
//
285+
// The result is:
286+
// verify(foo).bar()
287+
//
288+
// TODO(cpovirk): Suggest this only if `foo` looks like an actual mock object.
289+
SuggestedFix.Builder fix = SuggestedFix.builder();
290+
fix.postfixWith(maybeSelectOnMock.getExpression(), ")");
291+
int closingParen =
292+
reverse(state.getOffsetTokensForNode(invocationTree)).stream()
293+
.filter(t -> t.kind() == RPAREN)
294+
.findFirst()
295+
.get()
296+
.pos();
297+
fix.replace(closingParen, closingParen + 1, "");
298+
fixes.put(
299+
format("Verify that %s was called", maybeSelectOnMock.getIdentifier()), fix.build());
300+
}
301+
}
302+
}
303+
if (resultType != null && resultType.getKind() == TypeKind.BOOLEAN) {
304+
// Fix by calling either assertThat(...).isTrue() or verify(...).
305+
if (state.errorProneOptions().isTestOnlyTarget()) {
306+
SuggestedFix.Builder fix = SuggestedFix.builder();
307+
fix.prefixWith(
308+
invocationTree,
309+
qualifyStaticImport("com.google.common.truth.Truth.assertThat", fix, state) + "(")
310+
.postfixWith(invocationTree, ").isTrue()");
311+
fixes.put("Assert that the result is true", fix.build());
312+
} else {
313+
SuggestedFix.Builder fix = SuggestedFix.builder();
314+
fix.prefixWith(
315+
invocationTree,
316+
qualifyStaticImport("com.google.common.base.Verify.verify", fix, state) + "(")
317+
.postfixWith(invocationTree, ")");
318+
fixes.put("Insert a runtime check that the result is true", fix.build());
319+
}
320+
} else if (resultType != null
321+
// By looking for any isTrue() method, we handle not just Truth but also AssertJ.
322+
&& matchingMethods(
323+
NAME_OF_IS_TRUE.get(state),
324+
m -> m.getParameters().isEmpty(),
325+
resultType,
326+
state.getTypes())
327+
.anyMatch(m -> true)) {
328+
fixes.put("Assert that the result is true", postfixWith(invocationTree, ".isTrue()"));
329+
}
259330
if (identifierExpr != null
260331
&& symbol != null
261332
&& !symbol.name.contentEquals("this")
@@ -265,9 +336,31 @@ final ImmutableList<Fix> fixesAtCallSite(ExpressionTree invocationTree, VisitorS
265336
"Assign result back to variable",
266337
prefixWith(invocationTree, state.getSourceForNode(identifierExpr) + " = "));
267338
}
339+
/*
340+
* TODO(cpovirk): Suggest returning the value from the enclosing method where possible... *if*
341+
* we can find a good heuristic. We could consider "Is the return type a protobuf" and/or "Is
342+
* this a constructor call or build() call?"
343+
*/
344+
if (parent.getKind() == EXPRESSION_STATEMENT
345+
&& !constantExpressions.constantExpression(invocationTree, state).isPresent()) {
346+
ImmutableSet<String> identifiersInScope =
347+
findAllIdents(state).stream().map(v -> v.name.toString()).collect(toImmutableSet());
348+
concat(Stream.of("unused"), range(2, 10).mapToObj(i -> "unused" + i))
349+
// TODO(b/72928608): Handle even local variables declared *later* within this scope.
350+
// TODO(b/250568455): Also check whether we have suggested this name before in this scope.
351+
.filter(n -> !identifiersInScope.contains(n))
352+
.findFirst()
353+
.ifPresent(
354+
n ->
355+
fixes.put(
356+
"Suppress error by assigning to a variable",
357+
prefixWith(parent, format("var %s = ", n))));
358+
}
268359
if (parent.getKind() == EXPRESSION_STATEMENT) {
269360
if (constantExpressions.constantExpression(invocationTree, state).isPresent()) {
270361
fixes.put("Delete call", delete(parent));
362+
} else {
363+
fixes.put("Delete call and any side effects", delete(parent));
271364
}
272365
}
273366
return fixes.buildOrThrow().entrySet().stream()
@@ -523,4 +616,10 @@ public Description matchReturn(ReturnTree tree, VisitorState state) {
523616
}
524617
return NO_MATCH;
525618
}
619+
620+
private static final Matcher<ExpressionTree> MOCKITO_VERIFY =
621+
staticMethod().onClass("org.mockito.Mockito").named("verify");
622+
623+
private static final com.google.errorprone.suppliers.Supplier<com.sun.tools.javac.util.Name>
624+
NAME_OF_IS_TRUE = VisitorState.memoize(state -> state.getName("isTrue"));
526625
}

core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@
3030
import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL;
3131
import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.globalDefault;
3232
import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.mapAnnotationSimpleName;
33+
import static com.google.errorprone.fixes.SuggestedFix.emptyFix;
34+
import static com.google.errorprone.fixes.SuggestedFixes.qualifyType;
35+
import static com.google.errorprone.util.ASTHelpers.getAnnotationsWithSimpleName;
3336
import static com.google.errorprone.util.ASTHelpers.getSymbol;
3437
import static com.google.errorprone.util.ASTHelpers.getType;
3538
import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName;
39+
import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor;
40+
import static com.sun.source.tree.Tree.Kind.METHOD;
3641

3742
import com.google.common.collect.ImmutableMap;
3843
import com.google.errorprone.BugPattern;
@@ -44,6 +49,8 @@
4449
import com.google.errorprone.bugpatterns.checkreturnvalue.PackagesRule;
4550
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy;
4651
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator;
52+
import com.google.errorprone.fixes.Fix;
53+
import com.google.errorprone.fixes.SuggestedFix;
4754
import com.google.errorprone.matchers.Description;
4855
import com.google.errorprone.matchers.Matcher;
4956
import com.google.errorprone.util.ASTHelpers;
@@ -55,12 +62,16 @@
5562
import com.sun.source.tree.MethodInvocationTree;
5663
import com.sun.source.tree.MethodTree;
5764
import com.sun.source.tree.NewClassTree;
65+
import com.sun.source.util.TreePath;
66+
import com.sun.source.util.Trees;
5867
import com.sun.tools.javac.code.Symbol;
5968
import com.sun.tools.javac.code.Symbol.MethodSymbol;
6069
import com.sun.tools.javac.code.Type;
6170
import com.sun.tools.javac.code.Type.MethodType;
71+
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
6272
import java.util.Optional;
6373
import javax.lang.model.element.ElementKind;
74+
import org.checkerframework.checker.nullness.qual.Nullable;
6475

6576
/**
6677
* @author [email protected] (Eddie Aftandilian)
@@ -266,6 +277,7 @@ private Description describeInvocationResultIgnored(
266277
+ "%s",
267278
shortCall, shortCallWithoutNew, apiTrailer(symbol, state));
268279
return buildDescription(invocationTree)
280+
.addFix(fixAtDeclarationSite(symbol, state))
269281
.addAllFixes(fixesAtCallSite(invocationTree, state))
270282
.setMessage(message)
271283
.build();
@@ -336,7 +348,10 @@ protected Description describeReturnValueIgnored(MemberReferenceTree tree, Visit
336348
parensAndMaybeEllipsis,
337349
shortCallWithoutNew,
338350
apiTrailer(symbol, state));
339-
return buildDescription(tree).setMessage(message).build();
351+
return buildDescription(tree)
352+
.setMessage(message)
353+
.addFix(fixAtDeclarationSite(symbol, state))
354+
.build();
340355
}
341356

342357
private String apiTrailer(MethodSymbol symbol, VisitorState state) {
@@ -360,4 +375,31 @@ enum MessageTrailerStyle {
360375
NONE,
361376
API_ERASED_SIGNATURE,
362377
}
378+
379+
/** Returns a fix that adds {@code @CanIgnoreReturnValue} to the given symbol, if possible. */
380+
private static Fix fixAtDeclarationSite(MethodSymbol symbol, VisitorState state) {
381+
MethodTree method = findDeclaration(symbol, state);
382+
if (method == null || isGeneratedConstructor(method)) {
383+
return emptyFix();
384+
}
385+
SuggestedFix.Builder fix = SuggestedFix.builder();
386+
fix.prefixWith(
387+
method, "@" + qualifyType(state, fix, CanIgnoreReturnValue.class.getName()) + " ");
388+
getAnnotationsWithSimpleName(method.getModifiers().getAnnotations(), CHECK_RETURN_VALUE)
389+
.forEach(fix::delete);
390+
fix.setShortDescription("Annotate the method with @CanIgnoreReturnValue");
391+
return fix.build();
392+
}
393+
394+
private static @Nullable MethodTree findDeclaration(Symbol symbol, VisitorState state) {
395+
JavacProcessingEnvironment javacEnv = JavacProcessingEnvironment.instance(state.context);
396+
TreePath declPath = Trees.instance(javacEnv).getPath(symbol);
397+
// Skip fields declared in other compilation units since we can't make a fix for them here.
398+
if (declPath != null
399+
&& declPath.getCompilationUnit() == state.getPath().getCompilationUnit()
400+
&& (declPath.getLeaf().getKind() == METHOD)) {
401+
return (MethodTree) declPath.getLeaf();
402+
}
403+
return null;
404+
}
363405
}

core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ private static boolean isArgStaticAndConstant(ExpressionTree arg) {
390390
return (argSymbol.flags() & Flags.STATIC) != 0;
391391
}
392392

393+
// TODO(b/250568455): Make this more widely available.
393394
private static final class NameUniquifier {
394395
final Multiset<String> assignmentCounts = HashMultiset.create();
395396

0 commit comments

Comments
 (0)