Skip to content

Commit

Permalink
Create an evaluator for deciding if use of a method's result is expec…
Browse files Browse the repository at this point in the history
…ted (aka CRV) or optional (aka CIRV).

The idea here (for now) is to match the behavior of the `CheckReturnValue` checker (i.e. looking at the method, then enclosing classes, etc.) while improving on it in a number of ways:

- Make it easy to plug in new rules determining a result-use policy for methods.
- Make it easy to see what rules apply and how they're prioritized.
- Add a "global" scope that can be used for implementing defaults.
- Allow us to check declarations for conflicts between rules in general, not just to check for something being annotated with both CRV and CIRV. This should allow us to, for example, prevent an `@AutoValue` getter method from being annotated with CIRV.
- Make it far easier for us to get good data. Every rule can create an `Evaluation` with data that allows us to see exactly why any given method had a certain policy selected for it: the ID of the rule that selected the policy, the scope at which the selection was made, and the symbol that was evaluated to make that selection (for example the class that an annotation was found on).

See unknown commit for an example of how easy extending this with new rules should be.

PiperOrigin-RevId: 447130426
  • Loading branch information
cgdecker authored and Error Prone Team committed May 7, 2022
1 parent 67fe479 commit ba95fe7
Show file tree
Hide file tree
Showing 7 changed files with 586 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@
package com.google.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList;
import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.protoBuilders;
import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.EXPECTED;
import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL;
import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.globalDefault;
import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.mapAnnotationSimpleName;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue;
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy;
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
Expand All @@ -39,7 +45,6 @@
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import java.util.Optional;
import javax.lang.model.element.ElementKind;
Expand All @@ -58,64 +63,29 @@ public class CheckReturnValue extends AbstractReturnValueIgnored
private static final String CHECK_RETURN_VALUE = "CheckReturnValue";
private static final String CAN_IGNORE_RETURN_VALUE = "CanIgnoreReturnValue";

private static final ImmutableMap<String, CrvOpinion> ANNOTATIONS =
ImmutableMap.of(
CHECK_RETURN_VALUE,
CrvOpinion.SHOULD_BE_CRV,
CAN_IGNORE_RETURN_VALUE,
CrvOpinion.SHOULD_BE_CIRV);

private static Optional<FoundAnnotation> controllingAnnotation(
MethodSymbol sym, VisitorState visitorState) {
// In priority order, we want a source-local annotation on the symbol, then the external API
// file, then the enclosing elements of sym.
return findAnnotation(sym)
.or(
() ->
asAnnotationFromConfig(sym, visitorState)
.or(() -> findAnnotationOnEnclosingSymbols(sym.owner)));
}

private static Optional<FoundAnnotation> findAnnotation(Symbol sym) {
return ANNOTATIONS.entrySet().stream()
.filter(annoSpec -> hasDirectAnnotationWithSimpleName(sym, annoSpec.getKey()))
.map(annotation -> FoundAnnotation.create(scope(sym), annotation.getValue()))
.findFirst();
}

private static Optional<FoundAnnotation> findAnnotationOnEnclosingSymbols(Symbol sym) {
return ASTHelpers.enclosingElements(sym).flatMap(e -> findAnnotation(e).stream()).findFirst();
}

private static Optional<FoundAnnotation> asAnnotationFromConfig(
MethodSymbol sym, VisitorState visitorState) {
if (ExternalCanIgnoreReturnValue.externallyConfiguredCirvAnnotation(sym, visitorState)) {
return Optional.of(
FoundAnnotation.create(AnnotationScope.METHOD_EXTERNAL_ANNO, CrvOpinion.SHOULD_BE_CIRV));
}
return Optional.empty();
}

private static AnnotationScope scope(Symbol sym) {
if (sym instanceof MethodSymbol) {
return AnnotationScope.METHOD;
} else if (sym instanceof ClassSymbol) {
return AnnotationScope.CLASS;
} else {
return AnnotationScope.PACKAGE;
}
}

static final String CHECK_ALL_CONSTRUCTORS = "CheckReturnValue:CheckAllConstructors";
static final String CHECK_ALL_METHODS = "CheckReturnValue:CheckAllMethods";

private final boolean checkAllConstructors;
private final boolean checkAllMethods;
private final Optional<ResultUsePolicy> constructorPolicy;
private final Optional<ResultUsePolicy> methodPolicy;
private final ResultUsePolicyEvaluator evaluator;

public CheckReturnValue(ErrorProneFlags flags) {
super(flags);
this.checkAllConstructors = flags.getBoolean(CHECK_ALL_CONSTRUCTORS).orElse(false);
this.checkAllMethods = flags.getBoolean(CHECK_ALL_METHODS).orElse(false);
this.constructorPolicy = defaultPolicy(flags, CHECK_ALL_CONSTRUCTORS);
this.methodPolicy = defaultPolicy(flags, CHECK_ALL_METHODS);

this.evaluator =
ResultUsePolicyEvaluator.create(
mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED),
mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL),
protoBuilders(),
externalIgnoreList(),
globalDefault(methodPolicy, constructorPolicy));
}

private static Optional<ResultUsePolicy> defaultPolicy(ErrorProneFlags flags, String flag) {
return flags.getBoolean(flag).map(check -> check ? EXPECTED : OPTIONAL);
}

/**
Expand All @@ -124,49 +94,11 @@ public CheckReturnValue(ErrorProneFlags flags) {
*/
@Override
public Matcher<ExpressionTree> specializedMatcher() {
return (tree, state) -> {
Optional<MethodSymbol> maybeMethod = methodToInspect(tree);
if (maybeMethod.isEmpty()) {
return false;
}

return crvOpinionForMethod(maybeMethod.get(), tree, state)
.map(CrvOpinion.SHOULD_BE_CRV::equals)
.orElse(false);
};
}

private Optional<CrvOpinion> crvOpinionForMethod(
MethodSymbol sym, ExpressionTree tree, VisitorState state) {
Optional<FoundAnnotation> annotationForSymbol = controllingAnnotation(sym, state);
if (annotationForSymbol.isPresent()) {
return annotationForSymbol.map(FoundAnnotation::checkReturnValueOpinion);
}

// In the event there is no opinion from annotations, we use the checker's configuration to
// decide what the "default" for the universe is.
if (checkAllConstructors && sym.isConstructor()) {
return Optional.of(CrvOpinion.SHOULD_BE_CRV);
}

if (checkAllMethods) {
// There are carveouts for methods we know to be ignorable by default.
if (ModifiedButNotUsed.FLUENT_SETTER.matches(tree, state)) {
return Optional.of(CrvOpinion.SHOULD_BE_CIRV);
}
return Optional.of(CrvOpinion.SHOULD_BE_CRV);
}
// NB: You might consider this SHOULD_BE_CIRV (here, where the default is to not check any
// unannotated method, and no annotation exists)
// However, we also use this judgement in the "should be covered" part of the analysis, so we
// want to distinguish the states of "the world is CRV-by-default, but this method is annotated"
// from "the world is CIRV-by-default, and this method was unannotated".
return Optional.empty();
}

enum CrvOpinion {
SHOULD_BE_CRV,
SHOULD_BE_CIRV
return (tree, state) ->
methodToInspect(tree)
.map(method -> evaluator.evaluate(method, state))
.orElse(OPTIONAL)
.equals(EXPECTED);
}

private static Optional<MethodSymbol> methodToInspect(ExpressionTree tree) {
Expand Down Expand Up @@ -205,14 +137,18 @@ private static Optional<MethodSymbol> methodSymbol(ExpressionTree tree) {

@Override
public boolean isCovered(ExpressionTree tree, VisitorState state) {
return methodSymbol(tree).flatMap(sym -> crvOpinionForMethod(sym, tree, state)).isPresent();
return methodToInspect(tree).stream()
.flatMap(method -> evaluator.evaluations(method, state))
.findFirst()
.isPresent();
}

@Override
public ImmutableMap<String, ?> getMatchMetadata(ExpressionTree tree, VisitorState state) {
return methodSymbol(tree)
.flatMap(sym -> controllingAnnotation(sym, state))
.map(found -> ImmutableMap.of("annotation_scope", found.scope()))
return methodToInspect(tree).stream()
.flatMap(method -> evaluator.evaluations(method, state))
.findFirst()
.map(evaluation -> ImmutableMap.of("annotation_scope", evaluation.scope()))
.orElse(ImmutableMap.of());
}

Expand All @@ -234,6 +170,8 @@ public Description matchMethod(MethodTree tree, VisitorState state) {
boolean checkReturn = hasDirectAnnotationWithSimpleName(method, CHECK_RETURN_VALUE);
boolean canIgnore = hasDirectAnnotationWithSimpleName(method, CAN_IGNORE_RETURN_VALUE);

// TODO(cgdecker): We can check this with evaluator.checkForConflicts now, though I want to
// think more about how we build and format error messages in that.
if (checkReturn && canIgnore) {
return buildDescription(tree).setMessage(String.format(BOTH_ERROR, "method")).build();
}
Expand Down Expand Up @@ -274,15 +212,15 @@ && hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), CAN_IGNORE_RETU
@Override
protected String getMessage(Name name) {
return String.format(
checkAllMethods
methodPolicy.orElse(OPTIONAL).equals(EXPECTED)
? "Ignored return value of '%s', which wasn't annotated with @CanIgnoreReturnValue"
: "Ignored return value of '%s', which is annotated with @CheckReturnValue",
name);
}

@Override
protected Description describeReturnValueIgnored(NewClassTree newClassTree, VisitorState state) {
return checkAllConstructors
return constructorPolicy.orElse(OPTIONAL).equals(EXPECTED)
? buildDescription(newClassTree)
.setMessage(
String.format(
Expand All @@ -292,22 +230,4 @@ protected Description describeReturnValueIgnored(NewClassTree newClassTree, Visi
.build()
: super.describeReturnValueIgnored(newClassTree, state);
}

@AutoValue
abstract static class FoundAnnotation {
static FoundAnnotation create(AnnotationScope scope, CrvOpinion opinion) {
return new AutoValue_CheckReturnValue_FoundAnnotation(scope, opinion);
}

abstract AnnotationScope scope();

abstract CrvOpinion checkReturnValueOpinion();
}

enum AnnotationScope {
METHOD,
METHOD_EXTERNAL_ANNO,
CLASS,
PACKAGE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.common.io.LineProcessor;
import com.google.common.io.MoreFiles;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule;
import com.google.errorprone.suppliers.Supplier;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import java.io.IOException;
Expand All @@ -31,7 +32,13 @@
import java.util.Optional;

/** External source of information about @CanIgnoreReturnValue-equivalent API's. */
public class ExternalCanIgnoreReturnValue {
public final class ExternalCanIgnoreReturnValue extends MethodRule {

/** Returns a rule using an external list of APIs to ignore. */
public static ResultUseRule externalIgnoreList() {
return new ExternalCanIgnoreReturnValue();
}

private ExternalCanIgnoreReturnValue() {}

private static final String EXTERNAL_API_EXCLUSION_LIST = "CheckReturnValue:ApiExclusionList";
Expand All @@ -45,6 +52,18 @@ private ExternalCanIgnoreReturnValue() {}
.get(EXTERNAL_API_EXCLUSION_LIST)
.map(ExternalCanIgnoreReturnValue::tryLoadingConfigFile));

@Override
public String id() {
return "EXTERNAL_API_EXCLUSION_LIST";
}

@Override
public Optional<ResultUsePolicy> evaluateMethod(MethodSymbol method, VisitorState state) {
return externallyConfiguredCirvAnnotation(method, state)
? Optional.of(ResultUsePolicy.OPTIONAL)
: Optional.empty();
}

public static boolean externallyConfiguredCirvAnnotation(MethodSymbol m, VisitorState s) {
return EXTERNAL_RESOURCE.get(s).map(protoList -> protoList.methodMatches(m, s)).orElse(false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2022 The Error Prone Authors.
*
* 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.checkreturnvalue;

import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
import java.util.regex.Pattern;

/** Rules for methods on proto messages and builders. */
public final class ProtoRules {

private ProtoRules() {}

/**
* Returns a rule that handles proto builders, making their fluent setter methods' results
* ignorable.
*/
public static ResultUseRule protoBuilders() {
return new ProtoBuilder();
}

private static final Supplier<Type> MESSAGE_LITE_BUILDER =
supplier("com.google.protobuf.MessageLite.Builder");

// TODO(cgdecker): Move proto rules from IgnoredPureGetter and ReturnValueIgnored here

/** Rules for methods on proto builders. */
private static final class ProtoBuilder extends MethodRule {
private static final Pattern SETTERS = Pattern.compile("(add|clear|remove|set|put).+");

@Override
public String id() {
return "PROTO_BUILDER";
}

@Override
public Optional<ResultUsePolicy> evaluateMethod(MethodSymbol method, VisitorState state) {
Type ownerType = method.owner.type;
if (ASTHelpers.isSubtype(ownerType, MESSAGE_LITE_BUILDER.get(state), state)) {
if (SETTERS.matcher(method.name).matches()) {
return Optional.of(ResultUsePolicy.OPTIONAL);
}
}
return Optional.empty();
}
}

private static Supplier<Type> supplier(String name) {
return VisitorState.memoize(s -> s.getTypeFromString(name));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2022 The Error Prone Authors.
*
* 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.checkreturnvalue;

/** Policy for use of a method or constructor's result. */
public enum ResultUsePolicy {
/**
* Use of the result is expected except in certain contexts where the method is being used in a
* way such that not using the result is likely correct. Examples include when the result type at
* the callsite is {@code java.lang.Void} and when the surrounding context seems to be testing
* that the method throws an exception.
*/
EXPECTED,
/** Use of the result is optional. */
OPTIONAL,
}
Loading

0 comments on commit ba95fe7

Please sign in to comment.