diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 233441ab04c..d37dd2872ae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,20 +15,20 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
- java: [ 17, 11 ]
+ java: [ 18, 17, 11 ]
experimental: [ false ]
# Only test on macos and windows with a single recent JDK to avoid a
# combinatorial explosion of test configurations.
# Most OS-specific issues are not specific to a particular JDK version.
include:
- os: macos-latest
- java: 17
+ java: 18
experimental: false
- os: windows-latest
- java: 17
+ java: 18
experimental: false
- os: ubuntu-latest
- java: 18-ea
+ java: 19-ea
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
diff --git a/annotation/pom.xml b/annotation/pom.xml
index 59f69a89d89..656b494a015 100644
--- a/annotation/pom.xml
+++ b/annotation/pom.xml
@@ -21,7 +21,7 @@
com.google.errorproneerror_prone_parent
- HEAD-SNAPSHOT
+ 2.14.0@BugPattern annotation
diff --git a/annotations/pom.xml b/annotations/pom.xml
index 77a45ae44ea..ff9d9692300 100644
--- a/annotations/pom.xml
+++ b/annotations/pom.xml
@@ -21,7 +21,7 @@
com.google.errorproneerror_prone_parent
- HEAD-SNAPSHOT
+ 2.14.0error-prone annotations
diff --git a/annotations/src/main/java/com/google/errorprone/annotations/concurrent/LockMethod.java b/annotations/src/main/java/com/google/errorprone/annotations/concurrent/LockMethod.java
index a330a2e86c0..1abfdab2023 100644
--- a/annotations/src/main/java/com/google/errorprone/annotations/concurrent/LockMethod.java
+++ b/annotations/src/main/java/com/google/errorprone/annotations/concurrent/LockMethod.java
@@ -42,9 +42,12 @@
*
method-name(): The lock object is returned by calling the named nullary
* method.
*
+ *
+ * @deprecated the correctness of this annotation is not enforced; it will soon be removed.
*/
@Target(METHOD)
@Retention(CLASS)
+@Deprecated
public @interface LockMethod {
String[] value();
}
diff --git a/annotations/src/main/java/com/google/errorprone/annotations/concurrent/UnlockMethod.java b/annotations/src/main/java/com/google/errorprone/annotations/concurrent/UnlockMethod.java
index 05713d6a1f0..9437f66b9dd 100644
--- a/annotations/src/main/java/com/google/errorprone/annotations/concurrent/UnlockMethod.java
+++ b/annotations/src/main/java/com/google/errorprone/annotations/concurrent/UnlockMethod.java
@@ -42,9 +42,12 @@
*
method-name(): The lock object is returned by calling the named nullary
* method.
*
+ *
+ * @deprecated the correctness of this annotation is not enforced; it will soon be removed.
*/
@Target(METHOD)
@Retention(CLASS)
+@Deprecated
public @interface UnlockMethod {
String[] value();
}
diff --git a/check_api/pom.xml b/check_api/pom.xml
index 4a023c78089..da6a905acd3 100644
--- a/check_api/pom.xml
+++ b/check_api/pom.xml
@@ -21,7 +21,7 @@
com.google.errorproneerror_prone_parent
- HEAD-SNAPSHOT
+ 2.14.0error-prone check api
diff --git a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessAnnotations.java b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessAnnotations.java
index a7536a5ea01..1dfe998142e 100644
--- a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessAnnotations.java
+++ b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessAnnotations.java
@@ -41,13 +41,19 @@ public class NullnessAnnotations {
// TODO(kmb): Correctly handle JSR 305 @Nonnull(NEVER) etc.
private static final Predicate ANNOTATION_RELEVANT_TO_NULLNESS =
Pattern.compile(
- ".*\\b((Recently)?Nullable(Decl|Type)?|(Recently)?NotNull|NonNull(Decl|Type)?|"
- + "Nonnull|CheckForNull|PolyNull|MonotonicNonNull(Decl)?)$")
+ ".*\\b("
+ + "(Recently)?NotNull|NonNull(Decl|Type)?|Nonnull|"
+ + "(Recently)?Nullable(Decl|Type)?|CheckForNull|PolyNull|MonotonicNonNull(Decl)?|"
+ + "ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|"
+ + "ProtoPassThroughNullness"
+ + ")$")
.asPredicate();
private static final Predicate NULLABLE_ANNOTATION =
Pattern.compile(
".*\\b("
- + "(Recently)?Nullable(Decl|Type)?|CheckForNull|PolyNull|MonotonicNonNull(Decl)?"
+ + "(Recently)?Nullable(Decl|Type)?|CheckForNull|PolyNull|MonotonicNonNull(Decl)?|"
+ + "ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|"
+ + "ProtoPassThroughNullness"
+ ")$")
.asPredicate();
diff --git a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
index 88af29f9274..46509a70f54 100644
--- a/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
+++ b/check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessPropagationTransfer.java
@@ -30,8 +30,6 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
@@ -85,6 +83,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Predicate;
import javax.annotation.Nullable;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeVariable;
@@ -200,7 +199,7 @@ private static class ReturnValueIsNonNull implements Predicate, Seri
String.class.getName());
@Override
- public boolean apply(MethodInfo methodInfo) {
+ public boolean test(MethodInfo methodInfo) {
// Any method explicitly annotated is trusted to behave as advertised.
Optional fromAnnotations =
NullnessAnnotations.fromAnnotations(methodInfo.annotations());
@@ -308,7 +307,7 @@ public NullnessPropagationTransfer() {
* returning methods.
*/
public NullnessPropagationTransfer(Predicate additionalNonNullReturningMethods) {
- this(NULLABLE, Predicates.or(new ReturnValueIsNonNull(), additionalNonNullReturningMethods));
+ this(NULLABLE, new ReturnValueIsNonNull().or(additionalNonNullReturningMethods));
}
/**
@@ -789,7 +788,7 @@ private Nullness returnValueNullness(MethodInvocationNode node, @Nullable ClassA
return NONNULL;
}
- Nullness assumedNullness = methodReturnsNonNull.apply(callee) ? NONNULL : NULLABLE;
+ Nullness assumedNullness = methodReturnsNonNull.test(callee) ? NONNULL : NULLABLE;
if (!callee.isGenericResult) {
// We only care about inference results for methods that return a type variable.
return assumedNullness;
diff --git a/check_api/src/main/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcher.java b/check_api/src/main/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcher.java
index 8942712b3e5..259bbecf162 100644
--- a/check_api/src/main/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcher.java
+++ b/check_api/src/main/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcher.java
@@ -24,9 +24,7 @@
import static com.google.errorprone.matchers.Matchers.typeCast;
import static com.google.errorprone.util.ASTHelpers.constValue;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
-import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.isConsideredFinal;
-import static com.google.errorprone.util.ASTHelpers.isSubtype;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CompileTimeConstant;
@@ -35,6 +33,7 @@
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TypeCastTree;
@@ -112,16 +111,13 @@ public Boolean visitMethodInvocation(MethodInvocationTree tree, Void unused) {
public Boolean visitBinary(BinaryTree tree, Void unused) {
return defaultAction(tree, null)
|| (tree.getKind().equals(Kind.PLUS)
- // There's no principled reason not to extend this to non-String types, we're
- // just erring on the side of caution until further extensions are requested.
- && isString(tree.getLeftOperand())
- && isString(tree.getRightOperand())
&& tree.getLeftOperand().accept(this, null)
&& tree.getRightOperand().accept(this, null));
}
- private boolean isString(ExpressionTree tree) {
- return isSubtype(getType(tree), state.getSymtab().stringType, state);
+ @Override
+ public Boolean visitParenthesized(ParenthesizedTree tree, Void unused) {
+ return tree.getExpression().accept(this, null);
}
@Override
diff --git a/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java b/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java
index 34222fa5f41..179d4d2728c 100644
--- a/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java
+++ b/check_api/src/main/java/com/google/errorprone/matchers/UnusedReturnValueMatcher.java
@@ -35,10 +35,7 @@
import static com.google.errorprone.util.ASTHelpers.getResultType;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
-import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
-import static com.google.errorprone.util.ASTHelpers.isSameType;
import static com.google.errorprone.util.ASTHelpers.isVoidType;
-import static javax.lang.model.element.Modifier.ABSTRACT;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -57,7 +54,6 @@
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
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.Type;
import com.sun.tools.javac.code.Type.MethodType;
@@ -80,12 +76,7 @@ public final class UnusedReturnValueMatcher implements Matcher {
AllowReason.EXCEPTION_TESTING,
UnusedReturnValueMatcher::exceptionTesting,
AllowReason.RETURNS_JAVA_LANG_VOID,
- UnusedReturnValueMatcher::returnsJavaLangVoid,
- // TODO(kak): this exclusion really doesn't belong here, since the context of the calling
- // code doesn't matter; known builder setters are _always_ treated as CIRV, and the
- // surrounding context doesn't matter.
- AllowReason.KNOWN_BUILDER_SETTER,
- UnusedReturnValueMatcher::isKnownBuilderSetter);
+ UnusedReturnValueMatcher::returnsJavaLangVoid);
private static final ImmutableSet DISALLOW_EXCEPTION_TESTING =
Sets.immutableEnumSet(
@@ -158,44 +149,6 @@ public Stream getAllowReasons(ExpressionTree tree, VisitorState sta
.filter(reason -> ALLOW_MATCHERS.get(reason).matches(tree, state));
}
- /**
- * Returns {@code true} if the given tree is a method call to an abstract setter method inside of
- * a known builder class.
- */
- private static boolean isKnownBuilderSetter(ExpressionTree tree, VisitorState state) {
- Symbol symbol = getSymbol(tree);
- if (!(symbol instanceof MethodSymbol)) {
- return false;
- }
-
- // Avoid matching static Builder factory methods, like `static Builder newBuilder()`
- if (!symbol.getModifiers().contains(ABSTRACT)) {
- return false;
- }
-
- MethodSymbol method = (MethodSymbol) symbol;
- ClassSymbol enclosingClass = method.enclClass();
-
- // Setters always have exactly 1 param
- if (method.getParameters().size() != 1) {
- return false;
- }
-
- // If the enclosing class is not a known builder type, return false.
- if (!hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue.Builder", state)
- && !hasAnnotation(enclosingClass, "com.google.auto.value.AutoBuilder", state)) {
- return false;
- }
-
- // If the method return type is not the same as the enclosing type (the builder itself),
- // e.g., the abstract `build()` method on the Builder, return false.
- if (!isSameType(method.getReturnType(), enclosingClass.asType(), state)) {
- return false;
- }
-
- return true;
- }
-
private static boolean returnsJavaLangVoid(ExpressionTree tree, VisitorState state) {
return tree instanceof MemberReferenceTree
? returnsJavaLangVoid((MemberReferenceTree) tree, state)
diff --git a/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java b/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java
index 534a30a4a2c..7ff84c44ac9 100644
--- a/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java
+++ b/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java
@@ -1159,7 +1159,12 @@ public static ClassSymbol enclosingClass(Symbol sym) {
return sym.owner == null ? null : sym.owner.enclClass();
}
- /** Return the enclosing {@code PackageSymbol} of the given symbol, or {@code null}. */
+ /**
+ * Return the enclosing {@code PackageSymbol} of the given symbol, or {@code null}.
+ *
+ *
Prefer this to {@link Symbol#packge}, which throws a {@link NullPointerException} for
+ * symbols that are not contained by a package: https://bugs.openjdk.java.net/browse/JDK-8231911
+ */
@Nullable
public static PackageSymbol enclosingPackage(Symbol sym) {
Symbol curr = sym;
@@ -1782,7 +1787,7 @@ public Type visitCompoundAssignment(CompoundAssignmentTree tree, Void unused) {
break;
case PLUS_ASSIGNMENT:
Type stringType = state.getSymtab().stringType;
- if (types.isSameType(stringType, variableType)) {
+ if (types.isSuperType(variableType, stringType)) {
return stringType;
}
break;
diff --git a/check_api/src/main/java/com/google/errorprone/util/Visibility.java b/check_api/src/main/java/com/google/errorprone/util/Visibility.java
index e0c5ebb7931..6978b4396e5 100644
--- a/check_api/src/main/java/com/google/errorprone/util/Visibility.java
+++ b/check_api/src/main/java/com/google/errorprone/util/Visibility.java
@@ -17,6 +17,7 @@
package com.google.errorprone.util;
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
@@ -90,7 +91,7 @@ public boolean shouldBeVisible(Symbol symbol, VisitorState state) {
JCCompilationUnit compilationUnit = (JCCompilationUnit) state.getPath().getCompilationUnit();
PackageSymbol packge = compilationUnit.packge;
// TODO(ghm): Should we handle the default (unnamed) package here?
- return symbol.packge().equals(packge);
+ return enclosingPackage(symbol).equals(packge);
}
@Override
diff --git a/core/pom.xml b/core/pom.xml
index 813e137579f..0107e72486c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -21,7 +21,7 @@
com.google.errorproneerror_prone_parent
- HEAD-SNAPSHOT
+ 2.14.0error-prone library
@@ -355,6 +355,12 @@
${flogger.version}test
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+ test
+
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReferenceEquality.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReferenceEquality.java
index 4285eb1ec9e..279f7a4d7f3 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReferenceEquality.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReferenceEquality.java
@@ -38,6 +38,9 @@
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Kinds.KindSelector;
import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.TypeSymbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.util.Name;
import java.util.List;
import java.util.Optional;
@@ -81,6 +84,26 @@ public final Description matchBinary(BinaryTree tree, VisitorState state) {
return builder.build();
}
+ private static boolean symbolsTypeHasName(Symbol sym, String name) {
+ if (sym == null) {
+ return false;
+ }
+ Type type = sym.type;
+ if (type == null) {
+ return false;
+ }
+ TypeSymbol tsym = type.tsym;
+ if (tsym == null) {
+ return false;
+ }
+ Name typeName = tsym.getQualifiedName();
+ if (typeName == null) {
+ // Probably shouldn't happen, but might as well check
+ return false;
+ }
+ return typeName.contentEquals(name);
+ }
+
protected void addFixes(Description.Builder builder, BinaryTree tree, VisitorState state) {
ExpressionTree lhs = tree.getLeftOperand();
ExpressionTree rhs = tree.getRightOperand();
@@ -108,19 +131,9 @@ protected void addFixes(Description.Builder builder, BinaryTree tree, VisitorSta
if (nullness != NONNULL) {
Symbol existingObjects = FindIdentifiers.findIdent("Objects", state, KindSelector.TYP);
ObjectsFix preferredFix;
- if (existingObjects != null
- && existingObjects
- .type
- .tsym
- .getQualifiedName()
- .contentEquals(ObjectsFix.GUAVA.className)) {
+ if (symbolsTypeHasName(existingObjects, ObjectsFix.GUAVA.className)) {
preferredFix = ObjectsFix.GUAVA;
- } else if (existingObjects != null
- && existingObjects
- .type
- .tsym
- .getQualifiedName()
- .contentEquals(ObjectsFix.JAVA_UTIL.className)) {
+ } else if (symbolsTypeHasName(existingObjects, ObjectsFix.JAVA_UTIL.className)) {
preferredFix = ObjectsFix.JAVA_UTIL;
} else if (state.isAndroidCompatible()) {
preferredFix = ObjectsFix.GUAVA;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/BadAnnotationImplementation.java b/core/src/main/java/com/google/errorprone/bugpatterns/BadAnnotationImplementation.java
index b07359eea67..e15e99dff23 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/BadAnnotationImplementation.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/BadAnnotationImplementation.java
@@ -25,7 +25,6 @@
import static com.sun.source.tree.Tree.Kind.CLASS;
import static com.sun.source.tree.Tree.Kind.ENUM;
-import com.google.common.base.Predicate;
import com.google.common.base.Verify;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
@@ -44,6 +43,7 @@
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Name;
import java.lang.annotation.Annotation;
+import java.util.function.Predicate;
import javax.annotation.Nullable;
/**
@@ -83,28 +83,20 @@ public Description matchClass(ClassTree classTree, VisitorState state) {
Types types = state.getTypes();
Name equalsName = EQUALS.get(state);
Predicate equalsPredicate =
- new Predicate() {
- @Override
- public boolean apply(MethodSymbol methodSymbol) {
- return !methodSymbol.isStatic()
+ methodSymbol ->
+ !methodSymbol.isStatic()
&& ((methodSymbol.flags() & Flags.SYNTHETIC) == 0)
&& ((methodSymbol.flags() & Flags.ABSTRACT) == 0)
&& methodSymbol.getParameters().size() == 1
&& types.isSameType(
methodSymbol.getParameters().get(0).type, state.getSymtab().objectType);
- }
- };
Name hashCodeName = HASHCODE.get(state);
Predicate hashCodePredicate =
- new Predicate() {
- @Override
- public boolean apply(MethodSymbol methodSymbol) {
- return !methodSymbol.isStatic()
+ methodSymbol ->
+ !methodSymbol.isStatic()
&& ((methodSymbol.flags() & Flags.SYNTHETIC) == 0)
&& ((methodSymbol.flags() & Flags.ABSTRACT) == 0)
&& methodSymbol.getParameters().isEmpty();
- }
- };
for (Type sup : types.closure(ASTHelpers.getSymbol(classTree).type)) {
if (equals == null) {
@@ -134,7 +126,7 @@ private static MethodSymbol getMatchingMethod(
continue;
}
MethodSymbol methodSymbol = (MethodSymbol) sym;
- if (predicate.apply(methodSymbol)) {
+ if (predicate.test(methodSymbol)) {
return methodSymbol;
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/BanJNDI.java b/core/src/main/java/com/google/errorprone/bugpatterns/BanJNDI.java
new file mode 100644
index 00000000000..3b327a4a7e1
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/BanJNDI.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 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;
+
+import static com.google.errorprone.matchers.Matchers.anyMethod;
+import static com.google.errorprone.matchers.Matchers.anyOf;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+
+/** A {@link BugChecker} that detects use of the unsafe JNDI API system. */
+@BugPattern(
+ summary =
+ "Using JNDI may deserialize user input via the `Serializable` API which is extremely"
+ + " dangerous",
+ severity = SeverityLevel.ERROR)
+public final class BanJNDI extends BugChecker implements MethodInvocationTreeMatcher {
+
+ /** Checks for direct or indirect calls to context.lookup() via the JDK */
+ private static final Matcher MATCHER =
+ anyOf(
+ anyMethod()
+ .onDescendantOf("javax.naming.directory.DirContext")
+ .namedAnyOf(
+ "modifyAttributes",
+ "getAttributes",
+ "search",
+ "getSchema",
+ "getSchemaClassDefinition"),
+ anyMethod()
+ .onDescendantOf("javax.naming.Context")
+ .namedAnyOf("lookup", "bind", "rebind", "createSubcontext"),
+ anyMethod()
+ .onDescendantOf("javax.jdo.JDOHelperTest")
+ .namedAnyOf(
+ "testGetPMFBadJNDI",
+ "testGetPMFBadJNDIGoodClassLoader",
+ "testGetPMFNullJNDI",
+ "testGetPMFNullJNDIGoodClassLoader"),
+ anyMethod().onDescendantOf("javax.jdo.JDOHelper").named("getPersistenceManagerFactory"),
+ anyMethod()
+ .onDescendantOf("javax.management.remote.JMXConnectorFactory")
+ .named("connect"),
+ anyMethod().onDescendantOf("javax.sql.rowset.spi.SyncFactory").named("getInstance"),
+ anyMethod()
+ .onDescendantOf("javax.management.remote.rmi.RMIConnector.RMIClientCommunicatorAdmin")
+ .named("doStart"),
+ anyMethod().onDescendantOf("javax.management.remote.rmi.RMIConnector").named("connect"),
+ anyMethod().onDescendantOf("javax.naming.InitialContext").named("doLookup"));
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (state.errorProneOptions().isTestOnlyTarget() || !MATCHER.matches(tree, state)) {
+ return Description.NO_MATCH;
+ }
+
+ Description.Builder description = buildDescription(tree);
+
+ return description.build();
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/BanSerializableRead.java b/core/src/main/java/com/google/errorprone/bugpatterns/BanSerializableRead.java
index 31b84787532..de944b3e5fd 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/BanSerializableRead.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/BanSerializableRead.java
@@ -16,6 +16,7 @@
package com.google.errorprone.bugpatterns;
+import static com.google.errorprone.bugpatterns.SerializableReads.BANNED_OBJECT_INPUT_STREAM_METHODS;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.enclosingClass;
@@ -25,7 +26,6 @@
import static com.google.errorprone.matchers.Matchers.methodIsNamed;
import static com.google.errorprone.matchers.Matchers.not;
-import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.VisitorState;
@@ -41,28 +41,6 @@
severity = SeverityLevel.ERROR)
public final class BanSerializableRead extends BugChecker implements MethodInvocationTreeMatcher {
- private static final ImmutableSet BANNED_OBJECT_INPUT_STREAM_METHODS =
- ImmutableSet.of(
- // Prevent reading objects unsafely into memory
- "readObject",
-
- // This is the same, the default value
- "defaultReadObject",
-
- // This is for trusted subclasses
- "readObjectOverride",
-
- // Ultimately, a lot of the safety worries come
- // from being able to construct arbitrary classes via
- // reading in class descriptors. I don't think anyone
- // will bother calling this directly, but I don't see
- // any reason not to block it.
- "readClassDescriptor",
-
- // These are basically the same as above
- "resolveClass",
- "resolveObject");
-
private static final Matcher EXEMPT =
anyOf(
// This is called through ObjectInputStream; a call further up the callstack will have
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java
index be23462e7f1..1d12464638f 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java
@@ -17,18 +17,28 @@
package com.google.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoBuilders;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoValueBuilders;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.AutoValueRules.autoValues;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.mutableProtos;
+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.common.collect.ImmutableSet;
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.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;
@@ -39,10 +49,8 @@
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 java.util.stream.Stream;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
@@ -59,51 +67,46 @@ 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 ImmutableSet ANNOTATIONS =
- ImmutableSet.of(CHECK_RETURN_VALUE, CAN_IGNORE_RETURN_VALUE);
-
- private static Stream findAnnotation(Symbol sym) {
- return ANNOTATIONS.stream()
- .filter(annotation -> hasDirectAnnotationWithSimpleName(sym, annotation))
- .limit(1)
- .map(annotation -> FoundAnnotation.create(annotation, scope(sym)));
- }
-
- private static Optional firstAnnotation(MethodSymbol sym) {
- return ASTHelpers.enclosingElements(sym).flatMap(CheckReturnValue::findAnnotation).findFirst();
- }
-
- 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 Optional constructorPolicy;
+ private final Optional methodPolicy;
+ private final ResultUsePolicyEvaluator evaluator;
public CheckReturnValue(ErrorProneFlags flags) {
super(flags);
- this.checkAllConstructors = flags.getBoolean(CHECK_ALL_CONSTRUCTORS).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(),
+ mutableProtos(),
+ autoValues(),
+ autoValueBuilders(),
+ autoBuilders(),
+ externalIgnoreList(),
+ globalDefault(methodPolicy, constructorPolicy));
+ }
+
+ private static Optional defaultPolicy(ErrorProneFlags flags, String flag) {
+ return flags.getBoolean(flag).map(check -> check ? EXPECTED : OPTIONAL);
}
/**
- * Return a matcher for method invocations in which the method being called has the
- * {@code @CheckReturnValue} annotation.
+ * Return a matcher for method invocations in which the method being called should be considered
+ * must-be-used.
*/
@Override
public Matcher specializedMatcher() {
- return (tree, state) -> {
- Optional sym = methodToInspect(tree);
- return sym.flatMap(CheckReturnValue::firstAnnotation)
- .map(found -> found.annotation().equals(CHECK_RETURN_VALUE))
- .orElse(checkAllConstructors && sym.map(MethodSymbol::isConstructor).orElse(false));
- };
+ return (tree, state) ->
+ methodToInspect(tree)
+ .map(method -> evaluator.evaluate(method, state))
+ .orElse(OPTIONAL)
+ .equals(EXPECTED);
}
private static Optional methodToInspect(ExpressionTree tree) {
@@ -142,16 +145,23 @@ private static Optional methodSymbol(ExpressionTree tree) {
@Override
public boolean isCovered(ExpressionTree tree, VisitorState state) {
- return methodSymbol(tree)
- .map(m -> (checkAllConstructors && m.isConstructor()) || firstAnnotation(m).isPresent())
- .orElse(false);
+ return methodToInspect(tree).stream()
+ .flatMap(method -> evaluator.evaluations(method, state))
+ .findFirst()
+ .isPresent();
}
@Override
public ImmutableMap getMatchMetadata(ExpressionTree tree, VisitorState state) {
- return methodSymbol(tree)
- .flatMap(CheckReturnValue::firstAnnotation)
- .map(found -> ImmutableMap.of("annotation_scope", found.scope()))
+ return methodToInspect(tree).stream()
+ .flatMap(method -> evaluator.evaluations(method, state))
+ .findFirst()
+ .map(
+ evaluation ->
+ ImmutableMap.of(
+ "rule", evaluation.rule(),
+ "policy", evaluation.policy(),
+ "scope", evaluation.scope()))
.orElse(ImmutableMap.of());
}
@@ -173,6 +183,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();
}
@@ -213,12 +225,15 @@ && hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), CAN_IGNORE_RETU
@Override
protected String getMessage(Name name) {
return String.format(
- "Ignored return value of '%s', which is annotated with @CheckReturnValue", name);
+ 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(
@@ -228,21 +243,4 @@ protected Description describeReturnValueIgnored(NewClassTree newClassTree, Visi
.build()
: super.describeReturnValueIgnored(newClassTree, state);
}
-
- @AutoValue
- abstract static class FoundAnnotation {
- static FoundAnnotation create(String annotation, AnnotationScope scope) {
- return new AutoValue_CheckReturnValue_FoundAnnotation(annotation, scope);
- }
-
- abstract String annotation();
-
- abstract AnnotationScope scope();
- }
-
- enum AnnotationScope {
- METHOD,
- CLASS,
- PACKAGE
- }
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java
index c232bd0873f..b6e290efbcd 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ClassName.java
@@ -35,6 +35,7 @@
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
@BugPattern(
+ altNames = {"TopLevelName"},
summary = "The source file name should match the name of the top-level class it contains",
severity = ERROR,
documentSuppression = false,
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DivZero.java b/core/src/main/java/com/google/errorprone/bugpatterns/DivZero.java
deleted file mode 100644
index cac8cccb628..00000000000
--- a/core/src/main/java/com/google/errorprone/bugpatterns/DivZero.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2013 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;
-
-import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
-import static com.google.errorprone.matchers.Matchers.anyOf;
-import static com.google.errorprone.matchers.Matchers.kindIs;
-
-import com.google.errorprone.BugPattern;
-import com.google.errorprone.VisitorState;
-import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher;
-import com.google.errorprone.bugpatterns.BugChecker.CompoundAssignmentTreeMatcher;
-import com.google.errorprone.fixes.SuggestedFix;
-import com.google.errorprone.matchers.Description;
-import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.BinaryTree;
-import com.sun.source.tree.CompoundAssignmentTree;
-import com.sun.source.tree.ExpressionTree;
-import com.sun.source.tree.LiteralTree;
-import com.sun.source.tree.StatementTree;
-import com.sun.source.tree.Tree;
-import com.sun.source.tree.Tree.Kind;
-
-/**
- * Matches the behaviour of javac's divzero xlint warning.
- *
- * @author cushon@google.com (Liam Miller-Cushon)
- */
-@BugPattern(altNames = "divzero", summary = "Division by integer literal zero", severity = ERROR)
-public class DivZero extends BugChecker
- implements BinaryTreeMatcher, CompoundAssignmentTreeMatcher {
-
- @Override
- public Description matchBinary(BinaryTree tree, VisitorState state) {
- return matchDivZero(tree, tree.getRightOperand(), state);
- }
-
- @Override
- public Description matchCompoundAssignment(CompoundAssignmentTree tree, VisitorState state) {
- return matchDivZero(tree, tree.getExpression(), state);
- }
-
- private Description matchDivZero(Tree tree, ExpressionTree operand, VisitorState state) {
- if (!anyOf(kindIs(Kind.DIVIDE), kindIs(Kind.DIVIDE_ASSIGNMENT)).matches(tree, state)) {
- return Description.NO_MATCH;
- }
-
- if (!kindIs(Kind.INT_LITERAL).matches(operand, state)) {
- return Description.NO_MATCH;
- }
-
- LiteralTree rightOperand = (LiteralTree) operand;
- if (((Integer) rightOperand.getValue()) != 0) {
- return Description.NO_MATCH;
- }
-
- // Find and replace enclosing Statement.
- StatementTree enclosingStmt =
- ASTHelpers.findEnclosingNode(state.getPath(), StatementTree.class);
- return (enclosingStmt != null)
- ? describeMatch(
- tree,
- SuggestedFix.replace(enclosingStmt, "throw new ArithmeticException(\"/ by zero\");"))
- : describeMatch(tree);
- }
-}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/EmptyTopLevelDeclaration.java b/core/src/main/java/com/google/errorprone/bugpatterns/EmptyTopLevelDeclaration.java
index 81c04ca9be8..d4d0b8ded5b 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/EmptyTopLevelDeclaration.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/EmptyTopLevelDeclaration.java
@@ -16,8 +16,11 @@
package com.google.errorprone.bugpatterns;
-import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
@@ -25,28 +28,23 @@
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
-import java.util.ArrayList;
-import java.util.List;
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
-@BugPattern(summary = "Empty top-level type declaration", severity = WARNING)
-public class EmptyTopLevelDeclaration extends BugChecker implements CompilationUnitTreeMatcher {
+@BugPattern(summary = "Empty top-level type declarations should be omitted", severity = ERROR)
+public final class EmptyTopLevelDeclaration extends BugChecker
+ implements CompilationUnitTreeMatcher {
@Override
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
- List toDelete = new ArrayList<>();
- for (Tree member : tree.getTypeDecls()) {
- if (member.getKind() == Tree.Kind.EMPTY_STATEMENT) {
- toDelete.add(member);
- }
- }
+ ImmutableList toDelete =
+ tree.getTypeDecls().stream()
+ .filter(m -> m.getKind() == Tree.Kind.EMPTY_STATEMENT)
+ .collect(toImmutableList());
if (toDelete.isEmpty()) {
- return Description.NO_MATCH;
+ return NO_MATCH;
}
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
- for (Tree member : toDelete) {
- fixBuilder.delete(member);
- }
+ toDelete.forEach(fixBuilder::delete);
return describeMatch(toDelete.get(0), fixBuilder.build());
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpression.java b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpression.java
new file mode 100644
index 00000000000..7d3a0bf9ec9
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpression.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.getStartPosition;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.Tree.Kind;
+import java.util.Objects;
+
+/** A BugPattern; see the summary. */
+@BugPattern(
+ summary =
+ "This expression evaluates to 0. If this isn't an error, consider expressing it as a"
+ + " literal 0.",
+ severity = WARNING)
+public final class ErroneousBitwiseExpression extends BugChecker implements BinaryTreeMatcher {
+ @Override
+ public Description matchBinary(BinaryTree tree, VisitorState state) {
+ if (tree.getKind() != Kind.AND) {
+ return NO_MATCH;
+ }
+ Object constantValue = ASTHelpers.constValue(tree);
+ // Constants of the form A & B which evaluate to a literal 0 are probably trying to combine
+ // bitwise flags using |.
+ return Objects.equals(constantValue, 0) || Objects.equals(constantValue, 0L)
+ ? describeMatch(
+ tree,
+ SuggestedFix.replace(
+ /* startPos= */ state.getEndPosition(tree.getLeftOperand()),
+ /* endPos= */ getStartPosition(tree.getRightOperand()),
+ " | "))
+ : NO_MATCH;
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java
index 457dd65ede0..a3589134b29 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/FieldCanBeFinal.java
@@ -18,6 +18,7 @@
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.util.ASTHelpers.canBeRemoved;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
+import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
import static com.google.errorprone.util.ASTHelpers.shouldKeep;
import com.google.common.collect.ImmutableSet;
@@ -206,7 +207,6 @@ private VariableTree declaration() {
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
VariableAssignmentRecords writes = new VariableAssignmentRecords();
new FinalScanner(writes, state).scan(state.getPath(), InitializationContext.NONE);
- outer:
for (VariableAssignments var : writes.getAssignments()) {
if (!var.isEffectivelyFinal()) {
continue;
@@ -217,10 +217,8 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
if (shouldKeep(var.declaration)) {
continue;
}
- for (String annotation : IMPLICIT_VAR_ANNOTATIONS) {
- if (ASTHelpers.hasAnnotation(var.sym, annotation, state)) {
- continue outer;
- }
+ if (IMPLICIT_VAR_ANNOTATIONS.stream().anyMatch(a -> hasAnnotation(var.sym, a, state))) {
+ continue;
}
for (Attribute.Compound anno : var.sym.getAnnotationMirrors()) {
TypeElement annoElement = (TypeElement) anno.getAnnotationType().asElement();
@@ -233,12 +231,8 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
}
VariableTree varDecl = var.declaration();
SuggestedFixes.addModifiers(varDecl, state, Modifier.FINAL)
- .ifPresent(
- f -> {
- if (SuggestedFixes.compilesWithFix(f, state)) {
- state.reportMatch(describeMatch(varDecl, f));
- }
- });
+ .filter(f -> SuggestedFixes.compilesWithFix(f, state))
+ .ifPresent(f -> state.reportMatch(describeMatch(varDecl, f)));
}
return Description.NO_MATCH;
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java b/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java
index 31709559cb3..38f13775c18 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/HidingField.java
@@ -16,6 +16,7 @@
package com.google.errorprone.bugpatterns;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static java.util.stream.Collectors.toCollection;
@@ -136,7 +137,7 @@ private static boolean isPackagePrivateAndInDiffPackage(
&& !parentVariable.getModifiers().contains(Modifier.PROTECTED)
&& !parentVariable.getModifiers().contains(Modifier.PUBLIC)) { // package-private variable
- if (!parentVariable.packge().equals(getSymbol(currClass).packge())) {
+ if (!enclosingPackage(parentVariable).equals(getSymbol(currClass).packge())) {
return true;
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java b/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java
index 147ee181521..3b9e6f10f02 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java
@@ -57,22 +57,17 @@ public final class IgnoredPureGetter extends AbstractReturnValueIgnored {
VisitorState.memoize(
state -> state.getTypeFromString("com.google.protobuf.MutableMessageLite"));
- private final boolean checkAllProtos;
- private final boolean checkAutoBuilders;
-
public IgnoredPureGetter() {
this(ErrorProneFlags.empty());
}
public IgnoredPureGetter(ErrorProneFlags flags) {
super(flags);
- this.checkAllProtos = flags.getBoolean("IgnoredPureGetter:CheckAllProtos").orElse(true);
- this.checkAutoBuilders = flags.getBoolean("IgnoredPureGetter:CheckAutoBuilders").orElse(true);
}
@Override
protected Matcher super ExpressionTree> specializedMatcher() {
- return this::isPureGetter;
+ return IgnoredPureGetter::isPureGetter;
}
@Override
@@ -105,13 +100,11 @@ protected Description describeReturnValueIgnored(
return builder.build();
}
- // TODO(b/222475003): make this static again once the flag is gone
- private boolean isPureGetter(ExpressionTree tree, VisitorState state) {
+ private static boolean isPureGetter(ExpressionTree tree, VisitorState state) {
return pureGetterKind(tree, state).isPresent();
}
- // TODO(b/222475003): make this static again once the flag is gone
- private Optional pureGetterKind(ExpressionTree tree, VisitorState state) {
+ private static Optional pureGetterKind(ExpressionTree tree, VisitorState state) {
Symbol rawSymbol = getSymbol(tree);
if (!(rawSymbol instanceof MethodSymbol)) {
return Optional.empty();
@@ -126,8 +119,7 @@ private Optional pureGetterKind(ExpressionTree tree, VisitorStat
}
// The return value of any abstract method on an @AutoBuilder (which doesn't return the
// Builder itself) needs to be used.
- if (checkAutoBuilders
- && hasAnnotation(owner, "com.google.auto.value.AutoBuilder", state)
+ if (hasAnnotation(owner, "com.google.auto.value.AutoBuilder", state)
&& !isSameType(symbol.getReturnType(), owner.type, state)) {
return Optional.of(PureGetterKind.AUTO_BUILDER);
}
@@ -142,14 +134,7 @@ && hasAnnotation(owner, "com.google.auto.value.AutoBuilder", state)
try {
if (isSubtype(owner.type, MESSAGE_LITE.get(state), state)
&& !isSubtype(owner.type, MUTABLE_MESSAGE_LITE.get(state), state)) {
- String name = symbol.getSimpleName().toString();
- if ((name.startsWith("get") || name.startsWith("has"))
- && symbol.getParameters().isEmpty()) {
- return Optional.of(PureGetterKind.PROTO);
- }
- if (checkAllProtos) {
- return Optional.of(PureGetterKind.PROTO);
- }
+ return Optional.of(PureGetterKind.PROTO);
}
} catch (Symbol.CompletionFailure ignore) {
// isSubtype may throw this if some supertype's class file isn't found
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java b/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java
new file mode 100644
index 00000000000..56df7e5fe17
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java
@@ -0,0 +1,146 @@
+/*
+ * 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;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.VisitorState.memoize;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.matchers.Matchers.allOf;
+import static com.google.errorprone.matchers.Matchers.not;
+import static com.google.errorprone.matchers.Matchers.receiverOfInvocation;
+import static com.google.errorprone.matchers.Matchers.toType;
+import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
+import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
+import static com.google.errorprone.util.ASTHelpers.constValue;
+import static com.google.errorprone.util.ASTHelpers.enclosingClass;
+import static com.google.errorprone.util.ASTHelpers.getSymbol;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.suppliers.Supplier;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import java.util.Objects;
+import javax.lang.model.element.ElementKind;
+
+/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
+@BugPattern(
+ summary =
+ "Always pass 'false' to 'Future.cancel()', unless you are propagating a"
+ + " cancellation-with-interrupt from another caller",
+ severity = WARNING)
+public class Interruption extends BugChecker implements MethodInvocationTreeMatcher {
+
+ private static final Matcher CANCEL =
+ instanceMethod()
+ .onDescendantOfAny(
+ "java.util.concurrent.Future", "com.google.common.util.concurrent.ClosingFuture")
+ .named("cancel")
+ .withParameters("boolean");
+
+ private static final Matcher INTERRUPT_OTHER_THREAD =
+ allOf(
+ toType(
+ MethodInvocationTree.class,
+ instanceMethod()
+ .onDescendantOf("java.lang.Thread")
+ .named("interrupt")
+ .withNoParameters()),
+ not(
+ receiverOfInvocation(
+ staticMethod()
+ .onDescendantOf("java.lang.Thread")
+ .named("currentThread")
+ .withNoParameters())));
+
+ private static final Matcher WAS_INTERRUPTED =
+ instanceMethod()
+ .onDescendantOf("com.google.common.util.concurrent.AbstractFuture")
+ .named("wasInterrupted")
+ .withNoParameters();
+
+ private static final Supplier JAVA_UTIL_CONCURRENT_FUTURE =
+ memoize(state -> state.getSymbolFromString("java.util.concurrent.Future"));
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (state.errorProneOptions().isTestOnlyTarget()) {
+ return NO_MATCH;
+ }
+ // Thread.interrupt() other than Thread.currentThread().interrupt() (handles restoring an
+ // interrupt after catching it)
+ if (INTERRUPT_OTHER_THREAD.matches(tree, state)) {
+ return buildDescription(tree)
+ .setMessage(
+ "Thread.interrupt should not be called, except to record the interrupt status on the"
+ + " current thread when dealing with InterruptedException")
+ .build();
+ }
+ // Future.cancel calls ...
+ if (!CANCEL.matches(tree, state)) {
+ return NO_MATCH;
+ }
+ // ... unless they pass a literal 'false'
+ ExpressionTree argument = getOnlyElement(tree.getArguments());
+ if (Objects.equals(constValue(argument, Boolean.class), Boolean.FALSE)) {
+ return NO_MATCH;
+ }
+ // ... OR cancel(AbstractFuture.wasInterrupted())
+ if (WAS_INTERRUPTED.matches(argument, state)) {
+ return NO_MATCH;
+ }
+ // ... OR
+ //
+ // @Override
+ // public boolean cancel(boolean mayInterruptIfRunning) {
+ // ...
+ // someFuture.cancel(mayInterruptIfRunning);
+ // ...
+ // }
+ if (delegatingCancelMethod(state, argument)) {
+ return NO_MATCH;
+ }
+ return describeMatch(tree, SuggestedFix.replace(argument, "false"));
+ }
+
+ private boolean delegatingCancelMethod(VisitorState state, ExpressionTree argument) {
+ Symbol sym = getSymbol(argument);
+ if (sym == null) {
+ return false;
+ }
+ if (!sym.getKind().equals(ElementKind.PARAMETER)) {
+ return false;
+ }
+ MethodSymbol methodSymbol = (MethodSymbol) sym.owner;
+ if (methodSymbol.getParameters().size() != 1
+ || !methodSymbol.getSimpleName().contentEquals("cancel")) {
+ return false;
+ }
+ Symbol.ClassSymbol classSymbol = enclosingClass(methodSymbol);
+ if (!classSymbol.isSubClass(JAVA_UTIL_CONCURRENT_FUTURE.get(state), state.getTypes())) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java b/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java
index 302e5b47d60..23176749e12 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java
@@ -60,6 +60,7 @@
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
+import com.sun.tools.javac.util.Position;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -344,11 +345,13 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) {
if (escape[0]) {
return Optional.empty();
}
- return Optional.of(
- SuggestedFix.builder()
- .replace(newClassTree.getIdentifier(), "StringBuilder")
- .replace(varTree.getType(), "StringBuilder")
- .build());
+ SuggestedFix.Builder fix =
+ SuggestedFix.builder().replace(newClassTree.getIdentifier(), "StringBuilder");
+ if (ASTHelpers.getStartPosition(varTree.getType()) != Position.NOPOS) {
+ // If the variable is declared with `var`, there's no declaration type to change
+ fix = fix.replace(varTree.getType(), "StringBuilder");
+ }
+ return Optional.of(fix.build());
}
private static TreePath findEnclosingMethod(VisitorState state) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java b/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java
index 3abf519a4df..e77472ff220 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/MissingDefault.java
@@ -22,7 +22,6 @@
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
-import com.google.errorprone.BugPattern.StandardTags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.SwitchTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
@@ -43,8 +42,7 @@
"The Google Java Style Guide requires that each switch statement includes a default"
+ " statement group, even if it contains no code. (This requirement is lifted for any"
+ " switch statement that covers all values of an enum.)",
- severity = WARNING,
- tags = StandardTags.STYLE)
+ severity = WARNING)
public class MissingDefault extends BugChecker implements SwitchTreeMatcher {
@Override
public Description matchSwitch(SwitchTree tree, VisitorState state) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MixedArrayDimensions.java b/core/src/main/java/com/google/errorprone/bugpatterns/MixedArrayDimensions.java
index b971c4ee085..4c0bd66e7fa 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/MixedArrayDimensions.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/MixedArrayDimensions.java
@@ -18,6 +18,7 @@
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.enclosingClass;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import com.google.common.base.CharMatcher;
@@ -74,7 +75,8 @@ private Description checkArrayDimensions(Tree tree, Tree type, VisitorState stat
if (idx > nonWhitespace) {
String replacement = dim.substring(idx) + dim.substring(0, idx);
// SimpleCharStream generates violations in other packages, and is challenging to fix.
- if (getSymbol(tree).owner.name.contentEquals("SimpleCharStream")) {
+ var enclosingClass = enclosingClass(getSymbol(tree));
+ if (enclosingClass != null && enclosingClass.name.contentEquals("SimpleCharStream")) {
return NO_MATCH;
}
return describeMatch(tree, SuggestedFix.replace(start, end, replacement));
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java b/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java
index 3f316aa378f..19a89f4b58d 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java
@@ -20,6 +20,7 @@
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.isSubtype;
@@ -50,7 +51,7 @@
*/
@BugPattern(
summary =
- "The field number passed into #getFieldByNumber belongs to a different proto"
+ "The field number passed into #findFieldByNumber belongs to a different proto"
+ " to the Descriptor.",
severity = ERROR)
public final class MixedDescriptors extends BugChecker implements MethodInvocationTreeMatcher {
@@ -95,7 +96,7 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState
/** Ignore packages specifically qualified as proto1 or proto2. */
private static boolean shouldConsider(TypeSymbol symbol) {
- String packge = symbol.packge().toString();
+ String packge = enclosingPackage(symbol).toString();
return !(packge.contains(".proto1api") || packge.contains(".proto2api"));
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java b/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java
index 70e2df6bdfe..979dc093d36 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java
@@ -115,11 +115,11 @@ public class ModifiedButNotUsed extends BugChecker
private static final String MESSAGE_BUILDER = MESSAGE + ".Builder";
- private static final Matcher FLUENT_SETTER =
+ static final Matcher FLUENT_SETTER =
anyOf(
instanceMethod()
.onDescendantOf(MESSAGE_BUILDER)
- .withNameMatching(Pattern.compile("(add|clear|remove|set).+")),
+ .withNameMatching(Pattern.compile("(add|clear|merge|remove|set|put).*")),
instanceMethod()
.onDescendantOfAny(
GUAVA_IMMUTABLES.stream().map(c -> c + ".Builder").collect(toImmutableSet()))
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java b/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java
index 9c558eb6eb5..df53b31f8fc 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/MultipleTopLevelClasses.java
@@ -16,9 +16,9 @@
package com.google.errorprone.bugpatterns;
-import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
import com.google.common.base.Joiner;
import com.google.errorprone.BugPattern;
@@ -41,16 +41,11 @@
linkType = CUSTOM,
tags = StandardTags.STYLE,
link = "https://google.github.io/styleguide/javaguide.html#s3.4.1-one-top-level-class")
-public class MultipleTopLevelClasses extends BugChecker implements CompilationUnitTreeMatcher {
+public final class MultipleTopLevelClasses extends BugChecker
+ implements CompilationUnitTreeMatcher {
@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;
- }
-
List names = new ArrayList<>();
for (Tree member : tree.getTypeDecls()) {
if (member instanceof ClassTree) {
@@ -65,7 +60,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
// 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;
+ return NO_MATCH;
}
names.add(classMember.getSimpleName().toString());
break;
@@ -77,14 +72,15 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
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;
+ return NO_MATCH;
}
String message =
String.format(
"Expected at most one top-level class declaration, instead found: %s",
Joiner.on(", ").join(names));
- return buildDescription(firstNonNull(tree.getPackageName(), tree.getTypeDecls().get(0)))
- .setMessage(message)
- .build();
+ for (Tree typeDecl : tree.getTypeDecls()) {
+ state.reportMatch(buildDescription(typeDecl).setMessage(message).build());
+ }
+ return NO_MATCH;
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NonCanonicalType.java b/core/src/main/java/com/google/errorprone/bugpatterns/NonCanonicalType.java
index b9c67beef43..e7856277406 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/NonCanonicalType.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/NonCanonicalType.java
@@ -20,6 +20,7 @@
import static com.google.errorprone.fixes.SuggestedFixes.qualifyType;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
+import static com.google.errorprone.util.ASTHelpers.getStartPosition;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import com.google.errorprone.BugPattern;
@@ -33,6 +34,7 @@
import com.sun.source.tree.MemberSelectTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
+import com.sun.tools.javac.util.Position;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -64,6 +66,10 @@ public Description matchMemberSelect(MemberSelectTree tree, VisitorState state)
return NO_MATCH;
}
}
+ if (getStartPosition(tree) == Position.NOPOS) {
+ // Can't suggest changing a synthetic type tree
+ return NO_MATCH;
+ }
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
SuggestedFix fix =
fixBuilder
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NullableOnContainingClass.java b/core/src/main/java/com/google/errorprone/bugpatterns/NullableOnContainingClass.java
new file mode 100644
index 00000000000..1cef48a95b5
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/NullableOnContainingClass.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.getSymbol;
+import static com.google.errorprone.util.ASTHelpers.getType;
+import static java.util.Arrays.stream;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.tools.javac.code.Symbol;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+import java.util.List;
+
+/** A bugpattern; see the summary. */
+@BugPattern(
+ summary =
+ "Type-use nullability annotations should annotate the inner class, not the outer class"
+ + " (e.g., write `A.@Nullable B` instead of `@Nullable A.B`).",
+ severity = ERROR)
+public final class NullableOnContainingClass extends BugChecker
+ implements MemberSelectTreeMatcher, MethodTreeMatcher, VariableTreeMatcher {
+ @Override
+ public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
+ if (!(tree.getExpression() instanceof AnnotatedTypeTree)) {
+ return NO_MATCH;
+ }
+ return handle(((AnnotatedTypeTree) tree.getExpression()).getAnnotations(), tree, state);
+ }
+
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return handle(tree.getModifiers().getAnnotations(), tree.getReturnType(), state);
+ }
+
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ return handle(tree.getModifiers().getAnnotations(), tree.getType(), state);
+ }
+
+ private Description handle(
+ List extends AnnotationTree> annotations, Tree type, VisitorState state) {
+ if (!(type instanceof MemberSelectTree)) {
+ return NO_MATCH;
+ }
+ int endOfOuterType = state.getEndPosition(((MemberSelectTree) type).getExpression());
+
+ for (AnnotationTree annotation : annotations) {
+ if (!isTypeAnnotation(getSymbol(annotation))) {
+ continue;
+ }
+ if (NULLABLE_ANNOTATION_NAMES.contains(getType(annotation).tsym.getSimpleName().toString())) {
+ if (state.getEndPosition(annotation) < endOfOuterType) {
+ return describeMatch(
+ annotation,
+ SuggestedFix.builder()
+ .delete(annotation)
+ .replace(
+ endOfOuterType + 1,
+ endOfOuterType + 1,
+ state.getSourceForNode(annotation) + " ")
+ .build());
+ }
+ }
+ }
+ return NO_MATCH;
+ }
+
+ private static boolean isTypeAnnotation(Symbol anno) {
+ Target target = anno.getAnnotation(Target.class);
+ if (target == null) {
+ return false;
+ }
+ return stream(target.value()).anyMatch(t -> t.equals(ElementType.TYPE_USE));
+ }
+
+ private static final ImmutableSet NULLABLE_ANNOTATION_NAMES =
+ ImmutableSet.of("Nullable", "NonNull", "NullableType");
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java b/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java
index e625a66892d..32981eac2e0 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/OptionalNotPresent.java
@@ -48,8 +48,8 @@
*/
@BugPattern(
summary =
- "This Optional has been confirmed to be empty at this point, so the call to `get` will"
- + " throw.",
+ "This Optional has been confirmed to be empty at this point, so the call to `get()` or"
+ + " `orElseThrow()` will always throw.",
severity = WARNING)
public final class OptionalNotPresent extends BugChecker implements CompilationUnitTreeMatcher {
private static final Matcher OPTIONAL_GET =
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMap.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMap.java
deleted file mode 100644
index 0dd0975fe08..00000000000
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMap.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2018 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;
-
-import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
-
-import com.google.errorprone.BugPattern;
-import com.google.errorprone.VisitorState;
-import com.google.errorprone.suppliers.Supplier;
-import com.google.errorprone.util.ASTHelpers;
-import com.sun.tools.javac.code.Type;
-
-/**
- * Check for usage of {@code Set} or {@code Map}.
- *
- * @author seibelsabrina@google.com (Sabrina Seibel)
- */
-@BugPattern(
- summary =
- "Protos should not be used as a key to a map, in a set, or in a contains method on a "
- + "descendant of a collection. Protos have non deterministic ordering and proto "
- + "equality is deep, which is a performance issue.",
- severity = WARNING)
-public class ProtosAsKeyOfSetOrMap extends AbstractAsKeyOfSetOrMap {
-
- @Override
- protected boolean isBadType(Type type, VisitorState state) {
- return ASTHelpers.isSubtype(type, COM_GOOGLE_PROTOBUF_GENERATEDMESSAGE.get(state), state);
- }
-
- private static final Supplier COM_GOOGLE_PROTOBUF_GENERATEDMESSAGE =
- VisitorState.memoize(
- state -> state.getTypeFromString("com.google.protobuf.GeneratedMessage"));
-}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/RedundantOverride.java b/core/src/main/java/com/google/errorprone/bugpatterns/RedundantOverride.java
index f40f7d8b99e..4d085d6294f 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/RedundantOverride.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/RedundantOverride.java
@@ -19,6 +19,7 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.findSuperMethod;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
@@ -93,7 +94,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) {
}
// Overriding a protected member in another package broadens the visibility to the new package.
if (methodSymbol.getModifiers().contains(Modifier.PROTECTED)
- && !Objects.equals(superMethod.packge(), methodSymbol.packge())) {
+ && !Objects.equals(enclosingPackage(superMethod), enclosingPackage(methodSymbol))) {
return NO_MATCH;
}
// Exempt any change in annotations (aside from @Override).
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnValueIgnored.java b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnValueIgnored.java
index 37659844998..7d7e6da92e4 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnValueIgnored.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnValueIgnored.java
@@ -28,6 +28,7 @@
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import static com.google.errorprone.predicates.TypePredicates.isDescendantOf;
import static com.google.errorprone.predicates.TypePredicates.isExactTypeAny;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getReceiverType;
import static com.google.errorprone.util.ASTHelpers.getReturnType;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
@@ -131,7 +132,7 @@ private static boolean javaTimeTypes(ExpressionTree tree, VisitorState state) {
}
Symbol symbol = getSymbol(tree);
if (symbol instanceof MethodSymbol) {
- String qualifiedName = symbol.owner.packge().getQualifiedName().toString();
+ String qualifiedName = enclosingPackage(symbol.owner).getQualifiedName().toString();
return (qualifiedName.startsWith("java.time") || qualifiedName.startsWith("org.threeten.bp"))
&& symbol.getModifiers().contains(Modifier.PUBLIC)
&& !ALLOWED_JAVA_TIME_METHODS.matches(tree, state);
@@ -146,7 +147,7 @@ private static boolean javaTimeTypes(ExpressionTree tree, VisitorState state) {
private static boolean functionalMethod(ExpressionTree tree, VisitorState state) {
Symbol symbol = getSymbol(tree);
return symbol instanceof MethodSymbol
- && symbol.owner.packge().getQualifiedName().contentEquals("java.util.function");
+ && enclosingPackage(symbol.owner).getQualifiedName().contentEquals("java.util.function");
}
/**
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SerializableReads.java b/core/src/main/java/com/google/errorprone/bugpatterns/SerializableReads.java
new file mode 100644
index 00000000000..d61f67b23f4
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/SerializableReads.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableSet;
+
+/** List of banned methods for {@link BanSerializableRead}. */
+public final class SerializableReads {
+ private SerializableReads() {}
+
+ public static final ImmutableSet BANNED_OBJECT_INPUT_STREAM_METHODS =
+ ImmutableSet.of(
+ // Prevent reading objects unsafely into memory.
+ "readObject",
+
+ // This is the same, the default value.
+ "defaultReadObject",
+
+ // This is for trusted subclasses.
+ "readObjectOverride",
+
+ // Ultimately, a lot of the safety worries come from being able to construct arbitrary
+ // classes via reading in class descriptors. I don't think anyone will bother calling this
+ // directly, but I don't see any reason not to block it.
+ "readClassDescriptor",
+
+ // These are basically the same as above.
+ "resolveClass",
+ "resolveObject");
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/StaticImports.java b/core/src/main/java/com/google/errorprone/bugpatterns/StaticImports.java
index a04837dfd05..3c84d96e3da 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/StaticImports.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/StaticImports.java
@@ -17,6 +17,7 @@
package com.google.errorprone.bugpatterns;
import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import com.google.auto.value.AutoValue;
@@ -193,7 +194,7 @@ private static ImmutableSet lookup(
continue;
case 0:
case Flags.PROTECTED:
- if (member.packge() != pkg) {
+ if (enclosingPackage(member) != pkg) {
continue;
}
break;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SystemExitOutsideMain.java b/core/src/main/java/com/google/errorprone/bugpatterns/SystemExitOutsideMain.java
index 3e96c46c4c1..4b7bcd1b520 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/SystemExitOutsideMain.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/SystemExitOutsideMain.java
@@ -15,7 +15,7 @@
*/
package com.google.errorprone.bugpatterns;
-import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.enclosingMethod;
import static com.google.errorprone.matchers.Matchers.hasModifier;
@@ -49,7 +49,7 @@
*
* @author seibelsabrina@google.com (Sabrina Seibel)
*/
-@BugPattern(summary = "Code that contains System.exit() is untestable.", severity = WARNING)
+@BugPattern(summary = "Code that contains System.exit() is untestable.", severity = ERROR)
public class SystemExitOutsideMain extends BugChecker implements MethodInvocationTreeMatcher {
private static final Matcher CALLS_TO_SYSTEM_EXIT =
staticMethod().onClass("java.lang.System").named("exit");
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/TypesWithUndefinedEquality.java b/core/src/main/java/com/google/errorprone/bugpatterns/TypesWithUndefinedEquality.java
index 6d9eb9d6acb..1785dbb849e 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/TypesWithUndefinedEquality.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/TypesWithUndefinedEquality.java
@@ -24,25 +24,23 @@
/** Enumerates types which have poorly-defined behaviour for equals. */
public enum TypesWithUndefinedEquality {
+ // keep-sorted start
+ CHAR_SEQUENCE("CharSequence", "java.lang.CharSequence"),
+ COLLECTION("Collection", "java.util.Collection"),
+ DATE("Date", "java.util.Date"),
+ IMMUTABLE_COLLECTION("ImmutableCollection", "com.google.common.collect.ImmutableCollection"),
+ IMMUTABLE_MULTIMAP("ImmutableMultimap", "com.google.common.collect.ImmutableMultimap"),
+ ITERABLE("Iterable", "com.google.common.collect.FluentIterable", "java.lang.Iterable"),
LONG_SPARSE_ARRAY(
"LongSparseArray",
- "android.util.LongSparseArray",
"android.support.v4.util.LongSparseArrayCompat",
- "androidx.core.util.LongSparseArrayCompat",
- "androidx.collection.LongSparseArrayCompat"),
- SPARSE_ARRAY(
- "SparseArray",
- "android.util.SparseArray",
- "androidx.collection.SparseArrayCompat",
- "androidx.collection.SparseArrayCompat"),
+ "android.util.LongSparseArray",
+ "androidx.collection.LongSparseArrayCompat",
+ "androidx.core.util.LongSparseArrayCompat"),
MULTIMAP("Multimap", "com.google.common.collect.Multimap"),
- IMMUTABLE_MULTIMAP("ImmutableMultimap", "com.google.common.collect.ImmutableMultimap"),
- CHAR_SEQUENCE("CharSequence", "java.lang.CharSequence"),
- ITERABLE("Iterable", "java.lang.Iterable", "com.google.common.collect.FluentIterable"),
- COLLECTION("Collection", "java.util.Collection"),
- IMMUTABLE_COLLECTION("ImmutableCollection", "com.google.common.collect.ImmutableCollection"),
QUEUE("Queue", "java.util.Queue"),
- DATE("Date", "java.util.Date");
+ SPARSE_ARRAY("SparseArray", "android.util.SparseArray", "androidx.collection.SparseArrayCompat");
+ // keep-sorted end
private final String shortName;
private final ImmutableSet typeNames;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java b/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java
index 4da4012597c..c29df14641a 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/UngroupedOverloads.java
@@ -19,7 +19,6 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
-import static com.google.errorprone.BugPattern.StandardTags.STYLE;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
import static java.util.stream.Collectors.joining;
@@ -53,8 +52,7 @@
"Constructors and methods with the same name should appear sequentially with no other code"
+ " in between, even when modifiers such as static or private differ between the"
+ " methods. Please re-order or re-name methods.",
- severity = SUGGESTION,
- tags = STYLE)
+ severity = SUGGESTION)
public class UngroupedOverloads extends BugChecker implements ClassTreeMatcher {
private final Boolean batchFindings;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java
index a8ae6598291..bcb19bb6db1 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java
@@ -22,6 +22,7 @@
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.fixes.SuggestedFixes.prettyType;
import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getModifiers;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
@@ -183,12 +184,13 @@ private boolean canFix(Tree type, Symbol sym, VisitorState state) {
} catch (FunctionDescriptorLookupError e) {
return false;
}
- if (!PACKAGES_TO_FIX.contains(descriptor.packge().getQualifiedName().toString())) {
+ if (!PACKAGES_TO_FIX.contains(enclosingPackage(descriptor).getQualifiedName().toString())) {
return false;
}
class Scanner extends TreePathScanner {
boolean fixable = true;
+ boolean inInitializer = false;
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
@@ -196,6 +198,26 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
return super.visitMethodInvocation(node, null);
}
+ @Override
+ public Void visitVariable(VariableTree node, Void unused) {
+ boolean wasInInitializer = inInitializer;
+ if (sym.equals(getSymbol(node))) {
+ inInitializer = true;
+ }
+ super.visitVariable(node, null);
+ inInitializer = wasInInitializer;
+ return null;
+ }
+
+ @Override
+ public Void visitMemberSelect(MemberSelectTree node, Void unused) {
+ if (inInitializer && sym.equals(getSymbol(node))) {
+ // We're not smart enough to rewrite a recursive lambda.
+ fixable = false;
+ }
+ return super.visitMemberSelect(node, unused);
+ }
+
private void check(MethodInvocationTree node) {
ExpressionTree lhs = node.getMethodSelect();
if (!(lhs instanceof MemberSelectTree)) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java
index a49a1d121a8..f46a0859636 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java
@@ -24,8 +24,10 @@
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.StandardTags;
+import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.bugpatterns.threadsafety.ConstantExpressions;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
@@ -51,6 +53,17 @@
severity = WARNING,
tags = StandardTags.FRAGILE_CODE)
public class UnsynchronizedOverridesSynchronized extends BugChecker implements MethodTreeMatcher {
+
+ private final ConstantExpressions constantExpressions;
+
+ public UnsynchronizedOverridesSynchronized() {
+ this(ErrorProneFlags.empty());
+ }
+
+ public UnsynchronizedOverridesSynchronized(ErrorProneFlags flags) {
+ this.constantExpressions = ConstantExpressions.fromFlags(flags);
+ }
+
@Override
public Description matchMethod(MethodTree methodTree, VisitorState state) {
MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodTree);
@@ -86,8 +99,11 @@ private static boolean isSynchronized(MethodSymbol sym) {
return sym.getModifiers().contains(Modifier.SYNCHRONIZED);
}
- /** Don't flag methods that are empty or trivially delegate to a super-implementation. */
- private static boolean ignore(MethodTree method, VisitorState state) {
+ /**
+ * Don't flag methods that are empty, trivially delegate to a super-implementation, or return a
+ * constant.
+ */
+ private boolean ignore(MethodTree method, VisitorState state) {
return firstNonNull(
new TreeScanner() {
@Override
@@ -104,6 +120,11 @@ public Boolean visitBlock(BlockTree tree, Void unused) {
@Override
public Boolean visitReturn(ReturnTree tree, Void unused) {
+ ExpressionTree expression = tree.getExpression();
+ if (expression == null
+ || constantExpressions.constantExpression(expression, state).isPresent()) {
+ return true;
+ }
return scan(tree.getExpression(), null);
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java
index 48a3aef518d..e864aebc760 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedMethod.java
@@ -99,6 +99,7 @@ public final class UnusedMethod extends BugChecker implements CompilationUnitTre
"com.google.inject.Inject",
"com.google.inject.multibindings.ProvidesIntoMap",
"com.google.inject.multibindings.ProvidesIntoSet",
+ "com.tngtech.java.junit.dataprovider.DataProvider",
"javax.annotation.PreDestroy",
"javax.annotation.PostConstruct",
"javax.inject.Inject",
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java b/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java
index b56ec6e8ea8..57047eb3ff3 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/WrongOneof.java
@@ -20,24 +20,22 @@
import static com.google.common.collect.Iterables.getLast;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Description.NO_MATCH;
-import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.predicates.TypePredicates.isDescendantOf;
import static com.google.errorprone.util.ASTHelpers.enumValues;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
-import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
-import static com.google.errorprone.util.ASTHelpers.isConsideredFinal;
import static com.google.errorprone.util.ASTHelpers.stripParentheses;
import static com.google.errorprone.util.Reachability.canCompleteNormally;
import com.google.common.base.CaseFormat;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
+import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.SwitchTreeMatcher;
+import com.google.errorprone.bugpatterns.threadsafety.ConstantExpressions;
+import com.google.errorprone.bugpatterns.threadsafety.ConstantExpressions.ConstantExpression;
import com.google.errorprone.matchers.Description;
-import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.predicates.TypePredicate;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.ExpressionTree;
@@ -47,11 +45,8 @@
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.util.TreePath;
-import com.sun.tools.javac.code.Symbol;
-import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.HashSet;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
/** Matches always-default expressions in oneof switches. */
@@ -62,8 +57,11 @@ public final class WrongOneof extends BugChecker implements SwitchTreeMatcher {
private static final TypePredicate ONE_OF_ENUM =
isDescendantOf("com.google.protobuf.AbstractMessageLite.InternalOneOfEnum");
- private static final Matcher PROTO_METHOD =
- instanceMethod().onDescendantOf("com.google.protobuf.MessageLite");
+ private final ConstantExpressions constantExpressions;
+
+ public WrongOneof(ErrorProneFlags flags) {
+ this.constantExpressions = ConstantExpressions.fromFlags(flags);
+ }
@Override
public Description matchSwitch(SwitchTree tree, VisitorState state) {
@@ -78,12 +76,14 @@ public Description matchSwitch(SwitchTree tree, VisitorState state) {
if (receiver == null) {
return NO_MATCH;
}
- Optional> receiverSymbolChain =
- symbolizeImmutableExpression(receiver, state);
- if (!receiverSymbolChain.isPresent()) {
- return NO_MATCH;
- }
+ constantExpressions
+ .constantExpression(receiver, state)
+ .ifPresent(constantReceiver -> processSwitch(tree, constantReceiver, state));
+ return NO_MATCH;
+ }
+ private void processSwitch(
+ SwitchTree tree, ConstantExpression constantReceiver, VisitorState state) {
ImmutableSet getters =
enumValues(getType(tree.getExpression()).tsym).stream()
.map(WrongOneof::getter)
@@ -99,7 +99,7 @@ public Description matchSwitch(SwitchTree tree, VisitorState state) {
allowableGetters.add(
getter(((IdentifierTree) caseTree.getExpression()).getName().toString()));
- scanForInvalidGetters(getters, allowableGetters, caseTree, receiverSymbolChain.get(), state);
+ scanForInvalidGetters(getters, allowableGetters, caseTree, constantReceiver, state);
List extends StatementTree> statements = caseTree.getStatements();
if (statements != null
@@ -108,56 +108,13 @@ public Description matchSwitch(SwitchTree tree, VisitorState state) {
allowableGetters.clear();
}
}
- return NO_MATCH;
- }
-
- /**
- * Returns a list of the methods called to get to this proto expression, as well as a terminating
- * variable.
- *
- *
Absent if the chain of calls is not a sequence of immutable proto getters ending in an
- * effectively final variable.
- *
- *
For example {@code a.getFoo().getBar()} would return {@code MethodSymbol[getBar],
- * MethodSymbol[getFoo], VarSymbol[a]}.
- */
- private static Optional> symbolizeImmutableExpression(
- ExpressionTree tree, VisitorState state) {
- ImmutableList.Builder symbolized = ImmutableList.builder();
- ExpressionTree receiver = tree;
- while (true) {
- if (isPure(receiver, state)) {
- symbolized.add(getSymbol(receiver));
- } else {
- return Optional.empty();
- }
- if (receiver instanceof MethodInvocationTree || receiver instanceof MemberSelectTree) {
- receiver = getReceiver(receiver);
- } else {
- break;
- }
- }
- return Optional.of(symbolized.build());
- }
-
- private static boolean isPure(ExpressionTree receiver, VisitorState state) {
- if (receiver instanceof IdentifierTree) {
- Symbol symbol = getSymbol(receiver);
- return symbol instanceof VarSymbol && isConsideredFinal(symbol);
- }
- if (PROTO_METHOD.matches(receiver, state)) {
- // Ignore methods which take an argument, i.e. getters for repeated fields. We could check
- // that the argument is always the same, but...
- return ((MethodInvocationTree) receiver).getArguments().isEmpty();
- }
- return false;
}
private void scanForInvalidGetters(
Set getters,
Set allowableGetters,
CaseTree caseTree,
- ImmutableList receiverSymbolChain,
+ ConstantExpression receiverSymbolChain,
VisitorState state) {
new SuppressibleTreePathScanner(state) {
@Override
@@ -166,7 +123,8 @@ public Void visitMethodInvocation(MethodInvocationTree methodInvocationTree, Voi
if (receiver == null) {
return super.visitMethodInvocation(methodInvocationTree, null);
}
- if (!symbolizeImmutableExpression(receiver, state)
+ if (!constantExpressions
+ .constantExpression(receiver, state)
.map(receiverSymbolChain::equals)
.orElse(false)) {
return super.visitMethodInvocation(methodInvocationTree, null);
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java
new file mode 100644
index 00000000000..a134c030bbf
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java
@@ -0,0 +1,383 @@
+/*
+ * 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 static com.google.common.base.CharMatcher.whitespace;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.errorprone.matchers.Matchers.anyMethod;
+import static com.google.errorprone.matchers.Matchers.anyOf;
+import static com.google.errorprone.matchers.Matchers.constructor;
+import static java.lang.Character.isJavaIdentifierPart;
+import static java.lang.Character.isJavaIdentifierStart;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.annotations.CompileTimeConstant;
+import com.google.errorprone.matchers.Matcher;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.Type.ArrayType;
+import com.sun.tools.javac.code.Type.ClassType;
+import com.sun.tools.javac.code.Type.StructuralTypeMapping;
+import com.sun.tools.javac.code.TypeMetadata;
+import com.sun.tools.javac.code.Types;
+import java.util.List;
+
+/**
+ * Represents a Java method or constructor. Provides a method to parse an API from a string format,
+ * and another method to create an ErrorProne {@link Matcher} for the API.
+ */
+// TODO(kak): do we want to be able to represent classes in addition to methods/constructors?
+// TODO(kak): if not, then consider renaming to `MethodSignature` or something
+@AutoValue
+public abstract class Api {
+
+ /** Returns the {@code Api} representation of the given {@code symbol}. */
+ public static Api fromSymbol(MethodSymbol symbol, VisitorState state) {
+ Types types = state.getTypes();
+ return new AutoValue_Api(
+ symbol.owner.getQualifiedName().toString(),
+ symbol.name.toString(),
+ symbol.getParameters().stream()
+ .map(p -> fullyErasedAndUnannotatedType(p.type, types))
+ .collect(toImmutableList()));
+ }
+
+ static String fullyErasedAndUnannotatedType(Type type, Types types) {
+ // Removes type arguments, replacing w/ upper bounds
+ Type erasedType = types.erasureRecursive(type);
+ Type unannotatedType = erasedType.accept(ANNOTATION_REMOVER, null);
+ return unannotatedType.toString();
+ }
+
+ /**
+ * Removes type metadata (e.g.: type annotations) from types, as well as from "containing
+ * structures" like arrays. Notably, this annotation remover doesn't handle Type parameters, as it
+ * only attempts to handle erased types.
+ */
+ private static final StructuralTypeMapping ANNOTATION_REMOVER =
+ new StructuralTypeMapping<>() {
+ @Override
+ public Type visitType(Type t, Void unused) {
+ return t.baseType();
+ }
+
+ @Override
+ public Type visitClassType(ClassType t, Void unused) {
+ return super.visitClassType(t.cloneWithMetadata(TypeMetadata.EMPTY), unused);
+ }
+
+ // Remove annotations from all enclosing containers
+ @Override
+ public Type visitArrayType(ArrayType t, Void unused) {
+ return super.visitArrayType(t.cloneWithMetadata(TypeMetadata.EMPTY), unused);
+ }
+ };
+
+ // TODO(b/223668437): use this (or something other than the Matcher<> API)
+ static Matcher createMatcherFromApis(List apis) {
+ return anyOf(apis.stream().map(Api::parse).map(Api::matcher).collect(toImmutableList()));
+ }
+
+ static ImmutableSet createSetFromApis(List apis) {
+ return apis.stream().map(Api::parse).collect(toImmutableSet());
+ }
+
+ /** Returns the fully qualified type that contains the given method/constructor. */
+ public abstract String className();
+
+ /**
+ * Returns the simple name of the method. If the API is a constructor (i.e., {@code
+ * isConstructor() == true}), then {@code ""} is returned.
+ */
+ public abstract String methodName();
+
+ /** Returns the list of fully qualified parameter types for the given method/constructor. */
+ public abstract ImmutableList parameterTypes();
+
+ @Override
+ public final String toString() {
+ return String.format(
+ "%s#%s(%s)", className(), methodName(), Joiner.on(',').join(parameterTypes()));
+ }
+
+ /** Returns whether this API represents a constructor or not. */
+ boolean isConstructor() {
+ return methodName().equals("");
+ }
+
+ private Matcher matcher() {
+ return isConstructor()
+ ? constructor().forClass(className()).withParameters(parameterTypes())
+ : anyMethod()
+ .onClass(className())
+ .named(methodName())
+ // TODO(b/219754967): what about arrays
+ .withParameters(parameterTypes());
+ }
+
+ /**
+ * Parses an API string into an {@link Api}. Example API strings are:
+ *
+ *
+ *
a constructor (e.g., {@code java.net.URI#(java.lang.String)})
+ *
a static method (e.g., {@code java.net.URI#create(java.lang.String)})
+ *
an instance method (e.g., {@code java.util.List#get(int)})
+ *
an instance method with types erased (e.g., {@code java.util.List#add(java.lang.Object)})
+ *
+ */
+ @VisibleForTesting
+ public static Api parse(String api) {
+ return parse(api, false);
+ }
+
+ /**
+ * Parses an API string into an {@link Api}. Example API strings are:
+ *
+ *
+ *
a constructor (e.g., {@code java.net.URI#(java.lang.String)})
+ *
a static method (e.g., {@code java.net.URI#create(java.lang.String)})
+ *
an instance method (e.g., {@code java.util.List#get(int)})
+ *
an instance method with types erased (e.g., {@code java.util.List#add(java.lang.Object)})
+ *
+ */
+ static Api parse(String api, boolean assumeNoWhitespace) {
+ Parser p = new Parser(api, assumeNoWhitespace);
+
+ // Let's parse this in 3 parts:
+ // * Fully-qualified owning name, followed by #
+ // * method name, or "", followed by (
+ // * Any number of parameter types, all but the last followed by a ',', Finishing with )
+ // * and nothing at the end.
+
+ String className = p.owningType();
+ String methodName = p.methodName();
+ ImmutableList paramList = p.parameters();
+ p.ensureNoMoreCharacters();
+
+ return new AutoValue_Api(className, methodName, paramList);
+ }
+
+ private static final class Parser {
+ private final String api;
+ private final boolean assumeNoWhitespace;
+ private int position = -1;
+
+ Parser(String api, boolean assumeNoWhitespace) {
+ this.api = api;
+ this.assumeNoWhitespace = assumeNoWhitespace;
+ }
+
+ String owningType() {
+ StringBuilder buffer = new StringBuilder(api.length());
+ token:
+ do {
+ char next = nextLookingFor('#');
+ switch (next) {
+ case '#':
+ // We've hit the end of the leading type, break out.
+ break token;
+ case '.':
+ // OK, separator
+ break;
+ default:
+ checkArgument(
+ isJavaIdentifierPart(next),
+ "Unable to parse '%s' because '%s' is not a valid identifier",
+ api,
+ next);
+ }
+ buffer.append(next);
+ } while (true);
+ String type = buffer.toString();
+
+ check(!type.isEmpty(), api, "class name cannot be empty");
+ check(
+ isJavaIdentifierStart(type.charAt(0)),
+ api,
+ "the class name must start with a valid character");
+ return type;
+ }
+
+ String methodName() {
+ StringBuilder buffer = new StringBuilder(api.length() - position);
+ boolean isConstructor = false;
+ boolean finishedConstructor = false;
+ // match "", or otherwise a normal method name
+ token:
+ do {
+ char next = nextLookingFor('(');
+ switch (next) {
+ case '(':
+ // We've hit the end of the method name, break out.
+ break token;
+ case '<':
+ // Starting a constructor
+ check(!isConstructor, api, "Only one '<' is allowed");
+ check(buffer.length() == 0, api, "'<' must come directly after '#'");
+ isConstructor = true;
+ break;
+ case '>':
+ check(isConstructor, api, "'<' must come before '>'");
+ check(!finishedConstructor, api, "Only one '>' is allowed");
+ finishedConstructor = true;
+ break;
+ default:
+ checkArgument(
+ isJavaIdentifierPart(next),
+ "Unable to parse '%s' because '%s' is not a valid identifier",
+ api,
+ next);
+ }
+ buffer.append(next);
+ } while (true);
+
+ String methodName = buffer.toString();
+ if (isConstructor) {
+ check(finishedConstructor, api, "found '<' without closing '>");
+
+ // Must be "" exactly
+ checkArgument(
+ methodName.equals(""),
+ "Unable to parse '%s' because %s is an invalid method name",
+ api,
+ methodName);
+ } else {
+ check(!methodName.isEmpty(), api, "method name cannot be empty");
+ check(
+ isJavaIdentifierStart(methodName.charAt(0)),
+ api,
+ "the method name must start with a valid character");
+ }
+
+ return methodName;
+ }
+
+ ImmutableList parameters() {
+ // Text until the next ',' or ')' represents the parameter type.
+ // If the first token is ')', then we have an empty parameter list.
+ StringBuilder buffer = new StringBuilder(api.length() - position);
+ ImmutableList.Builder paramBuilder = ImmutableList.builder();
+ boolean emptyList = true;
+ paramList:
+ do {
+ char next = nextLookingFor(')');
+ switch (next) {
+ case ')':
+ if (emptyList) {
+ return ImmutableList.of();
+ }
+ // We've hit the end of the whole list, bail out.
+ paramBuilder.add(consumeParam(buffer));
+ break paramList;
+ case ',':
+ // We've hit the middle of a parameter, consume it
+ paramBuilder.add(consumeParam(buffer));
+ break;
+
+ case '[':
+ case ']':
+ case '.':
+ // . characters are separators, [ and ] are array characters, they're checked @ the end
+ buffer.append(next);
+ break;
+
+ default:
+ checkArgument(
+ isJavaIdentifierPart(next),
+ "Unable to parse '%s' because '%s' is not a valid identifier",
+ api,
+ next);
+ emptyList = false;
+ buffer.append(next);
+ }
+ } while (true);
+ return paramBuilder.build();
+ }
+
+ private String consumeParam(StringBuilder buffer) {
+ String parameter = buffer.toString();
+ buffer.setLength(0); // reset the buffer
+ check(!parameter.isEmpty(), api, "parameters cannot be empty");
+
+ check(
+ isJavaIdentifierStart(parameter.charAt(0)),
+ api,
+ "parameters must start with a valid character");
+
+ // Array specs must be in balanced pairs at the *end* of the parameter.
+ boolean parsingArrayStart = false;
+ boolean hasArraySpecifiers = false;
+ for (int k = 1; k < parameter.length(); k++) {
+ char c = parameter.charAt(k);
+ switch (c) {
+ case '[':
+ check(!parsingArrayStart, api, "multiple consecutive [");
+ hasArraySpecifiers = true;
+ parsingArrayStart = true;
+ break;
+ case ']':
+ check(parsingArrayStart, api, "unbalanced ] in array type");
+ parsingArrayStart = false;
+ break;
+ default:
+ check(
+ !hasArraySpecifiers,
+ api,
+ "types with array specifiers should end in those specifiers");
+ }
+ }
+ check(!parsingArrayStart, api, "[ without closing ] at the end of a parameter type");
+ return parameter;
+ }
+
+ // skip whitespace characters and give the next non-whitespace character. If we hit the end
+ // without a non-whitespace character, throw expecting the delimiter.
+ private char nextLookingFor(char delimiter) {
+ char next;
+ do {
+ position++;
+ checkArgument(
+ position < api.length(), "Could not parse '%s' as it must contain an '%s'", delimiter);
+ next = api.charAt(position);
+ } while (!assumeNoWhitespace && whitespace().matches(next));
+ return next;
+ }
+
+ void ensureNoMoreCharacters() {
+ if (assumeNoWhitespace) {
+ return;
+ }
+
+ while (++position < api.length()) {
+ check(whitespace().matches(api.charAt(position)), api, "it should end in ')'");
+ }
+ }
+ }
+
+ // The @CompileTimeConstant is for performance - reason should be constant and not eagerly
+ // constructed.
+ private static void check(boolean condition, String api, @CompileTimeConstant String reason) {
+ checkArgument(condition, "Unable to parse '%s' because %s", api, reason);
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/AutoValueRules.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/AutoValueRules.java
new file mode 100644
index 00000000000..55c6a49b546
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/AutoValueRules.java
@@ -0,0 +1,110 @@
+/*
+ * 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 static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.EXPECTED;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL;
+import static com.google.errorprone.util.ASTHelpers.enclosingClass;
+import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
+import static com.google.errorprone.util.ASTHelpers.isSameType;
+import static com.sun.tools.javac.code.Flags.ABSTRACT;
+
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule;
+import com.sun.tools.javac.code.Symbol.ClassSymbol;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import java.util.Optional;
+
+/** Rules for {@code @AutoValue}, {@code @AutoValue.Builder}, and {@code @AutoBuilder} types. */
+public final class AutoValueRules {
+ private AutoValueRules() {}
+
+ /** Returns a rule for {@code abstract} methods on {@code @AutoValue} types. */
+ public static ResultUseRule autoValues() {
+ return new ValueRule();
+ }
+
+ /** Returns a rule for {@code abstract} methods on {@code @AutoValue.Builder} types. */
+ public static ResultUseRule autoValueBuilders() {
+ return new BuilderRule("AutoValue.Builder");
+ }
+
+ /** Returns a rule for {@code abstract} methods on {@code @AutoBuilder} types. */
+ public static ResultUseRule autoBuilders() {
+ return new BuilderRule("AutoBuilder");
+ }
+
+ private static final class ValueRule extends AbstractAutoRule {
+ ValueRule() {
+ super("AutoValue");
+ }
+
+ @Override
+ protected ResultUsePolicy autoMethodPolicy(
+ MethodSymbol abstractMethod, ClassSymbol autoClass, VisitorState state) {
+ return EXPECTED;
+ }
+ }
+
+ private static final class BuilderRule extends AbstractAutoRule {
+ BuilderRule(String annotation) {
+ super(annotation);
+ }
+
+ @Override
+ protected ResultUsePolicy autoMethodPolicy(
+ MethodSymbol abstractMethod, ClassSymbol autoClass, VisitorState state) {
+ return abstractMethod.getParameters().size() == 1
+ && isSameType(abstractMethod.getReturnType(), autoClass.type, state)
+ ? OPTIONAL
+ : EXPECTED;
+ }
+ }
+
+ private abstract static class AbstractAutoRule extends MethodRule {
+ private static final String PACKAGE = "com.google.auto.value.";
+
+ private final String annotation;
+
+ AbstractAutoRule(String annotation) {
+ this.annotation = annotation;
+ }
+
+ @Override
+ public String id() {
+ return '@' + annotation;
+ }
+
+ protected abstract ResultUsePolicy autoMethodPolicy(
+ MethodSymbol abstractMethod, ClassSymbol autoClass, VisitorState state);
+
+ private static boolean isAbstract(MethodSymbol method) {
+ return (method.flags() & ABSTRACT) != 0;
+ }
+
+ @Override
+ public Optional evaluateMethod(MethodSymbol method, VisitorState state) {
+ if (isAbstract(method)) {
+ ClassSymbol enclosingClass = enclosingClass(method);
+ if (hasAnnotation(enclosingClass, PACKAGE + annotation, state)) {
+ return Optional.of(autoMethodPolicy(method, enclosingClass, state));
+ }
+ }
+ return Optional.empty();
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java
new file mode 100644
index 00000000000..b64fc90de99
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java
@@ -0,0 +1,177 @@
+/*
+ * 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 static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.Api.fullyErasedAndUnannotatedType;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.io.CharSource;
+import com.google.common.io.MoreFiles;
+import com.google.errorprone.ErrorProneFlags;
+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 com.sun.tools.javac.code.Symbol.VarSymbol;
+import com.sun.tools.javac.code.Types;
+import com.sun.tools.javac.util.List;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/** External source of information about @CanIgnoreReturnValue-equivalent API's. */
+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";
+ private static final String EXCLUSION_LIST_PARSER = "CheckReturnValue:ApiExclusionListParser";
+
+ private static final Supplier EXTERNAL_RULE_EVALUATOR =
+ VisitorState.memoize(
+ state ->
+ state
+ .errorProneOptions()
+ .getFlags()
+ .get(EXTERNAL_API_EXCLUSION_LIST)
+ .map(
+ filename ->
+ loadConfigListFromFile(filename, state.errorProneOptions().getFlags()))
+ .orElse((m, s) -> false));
+
+ @Override
+ public String id() {
+ return "EXTERNAL_API_EXCLUSION_LIST";
+ }
+
+ @Override
+ public Optional evaluateMethod(MethodSymbol method, VisitorState state) {
+ return EXTERNAL_RULE_EVALUATOR.get(state).methodMatches(method, state)
+ ? Optional.of(ResultUsePolicy.OPTIONAL)
+ : Optional.empty();
+ }
+
+ /** Encapsulates asking "does this API match the list of APIs I care about"? */
+ @FunctionalInterface
+ private interface MethodPredicate {
+ boolean methodMatches(MethodSymbol methodSymbol, VisitorState state);
+ }
+
+ // TODO(b/232240203): Api Parsing at analysis time is expensive - there are many ways to
+ // load and use the config file.
+ // Decide on what works best, taking into account hit rate, load time, etc.
+ enum ConfigParser {
+ AS_STRINGS {
+ @Override
+ MethodPredicate load(CharSource file) throws IOException {
+ return configByInterpretingMethodsAsStrings(file);
+ }
+ },
+ PARSE_TOKENS {
+ @Override
+ MethodPredicate load(CharSource file) throws IOException {
+ return configByParsingApiObjects(file);
+ }
+ };
+
+ abstract MethodPredicate load(CharSource file) throws IOException;
+ }
+
+ private static MethodPredicate loadConfigListFromFile(String filename, ErrorProneFlags flags) {
+ ConfigParser configParser =
+ flags.getEnum(EXCLUSION_LIST_PARSER, ConfigParser.class).orElse(ConfigParser.AS_STRINGS);
+ try {
+ CharSource file = MoreFiles.asCharSource(Paths.get(filename), UTF_8);
+ return configParser.load(file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(
+ "Could not load external resource for CanIgnoreReturnValue", e);
+ }
+ }
+
+ private static MethodPredicate configByInterpretingMethodsAsStrings(CharSource file)
+ throws IOException {
+ ImmutableSet apis;
+ // NB: No whitespace stripping here
+ try (Stream lines = file.lines()) {
+ apis = lines.collect(toImmutableSet());
+ }
+ return new MethodPredicate() {
+ @Override
+ public boolean methodMatches(MethodSymbol methodSymbol, VisitorState state) {
+ // Construct an API identifier for this method, which involves erasing parameter types
+ return apis.contains(apiSignature(methodSymbol, state.getTypes()));
+ }
+
+ private String apiSignature(MethodSymbol methodSymbol, Types types) {
+ return methodSymbol.owner.getQualifiedName()
+ + "#"
+ + methodSymbol.name
+ + "("
+ + paramsString(methodSymbol, types)
+ + ")";
+ }
+
+ private String paramsString(MethodSymbol symbol, Types types) {
+ if (symbol.params().isEmpty()) {
+ return "";
+ }
+ return String.join(
+ ",",
+ Iterables.transform(
+ symbol.params(), p -> fullyErasedAndUnannotatedType(p.type, types)));
+ }
+ };
+ }
+
+ private static MethodPredicate configByParsingApiObjects(CharSource file) throws IOException {
+ ImmutableSetMultimap apis;
+ try (Stream lines = file.lines()) {
+ apis =
+ lines
+ .map(l -> Api.parse(l, /* assumeNoWhitespace= */ true))
+ .collect(toImmutableSetMultimap(Api::className, api -> api));
+ }
+ return (methodSymbol, state) ->
+ apis.get(methodSymbol.enclClass().getQualifiedName().toString()).stream()
+ .anyMatch(
+ api ->
+ methodSymbol.getSimpleName().contentEquals(api.methodName())
+ && methodParametersMatch(
+ api.parameterTypes(), methodSymbol.params(), state.getTypes()));
+ }
+
+ private static boolean methodParametersMatch(
+ ImmutableList parameters, List methodParams, Types types) {
+ return Iterables.elementsEqual(
+ parameters,
+ Iterables.transform(methodParams, p -> fullyErasedAndUnannotatedType(p.type, types)));
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java
new file mode 100644
index 00000000000..ae3c334d82b
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java
@@ -0,0 +1,102 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.errorprone.util.ASTHelpers.isSubtype;
+
+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 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 ProtoRule(supplier("com.google.protobuf.MessageLite.Builder"), "PROTO_BUILDER");
+ }
+
+ /**
+ * Returns a rule that handles mutable protos, making their fluent setter methods' results
+ * ignorable.
+ */
+ public static ResultUseRule mutableProtos() {
+ return new ProtoRule(
+ supplier("com.google.protobuf.AbstractMutableMessageLite"), "MUTABLE_PROTO");
+ }
+
+ // TODO(cgdecker): Move proto rules from IgnoredPureGetter and ReturnValueIgnored here
+
+ /** Rules for methods on protos. */
+ private static final class ProtoRule extends MethodRule {
+ private static final Pattern RETURNS_THIS =
+ Pattern.compile("(add|clear|merge|remove|set|put).*");
+
+ private final Supplier parentType;
+ private final String id;
+
+ ProtoRule(Supplier parentType, String id) {
+ this.parentType = checkNotNull(parentType);
+ this.id = checkNotNull(id);
+ }
+
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public Optional evaluateMethod(MethodSymbol method, VisitorState state) {
+ if (isProtoSubtype(method.owner.type, state)) {
+ String methodName = method.name.toString();
+ if (RETURNS_THIS.matcher(methodName).matches()) {
+ return Optional.of(ResultUsePolicy.OPTIONAL);
+ }
+ if (isGetterOfSubmessageBuilder(methodName)
+ && isProtoSubtype(method.getReturnType(), state)) {
+ return Optional.of(ResultUsePolicy.OPTIONAL);
+ }
+ }
+ return Optional.empty();
+ }
+
+ private boolean isProtoSubtype(Type ownerType, VisitorState state) {
+ return isSubtype(ownerType, parentType.get(state), state);
+ }
+
+ // fooBuilder.getBarBuilder() mutates the builder such that foo.hasBar() is now true.
+ private static boolean isGetterOfSubmessageBuilder(String name) {
+ // TODO(glorioso): Any other naming conventions to check?
+ // TODO(glorioso): Maybe worth making this a regex instead? But think about performance
+ return name.startsWith("get") && name.endsWith("Builder") && !name.endsWith("OrBuilder");
+ }
+ }
+
+ private static Supplier supplier(String name) {
+ return VisitorState.memoize(s -> s.getTypeFromString(name));
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicy.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicy.java
new file mode 100644
index 00000000000..145844b618b
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicy.java
@@ -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,
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicyEvaluator.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicyEvaluator.java
new file mode 100644
index 00000000000..bf79646afa7
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicyEvaluator.java
@@ -0,0 +1,132 @@
+/*
+ * 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 static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.ENCLOSING_ELEMENTS;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.GLOBAL;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.METHOD;
+import static java.util.Map.entry;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.Evaluation;
+import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.stream.Stream;
+import javax.lang.model.element.ElementKind;
+
+/**
+ * Evaluates methods and their enclosing classes and packages to determine a {@link ResultUsePolicy}
+ * for the methods.
+ */
+public final class ResultUsePolicyEvaluator {
+
+ /** Creates a new {@link ResultUsePolicyEvaluator} using the given {@code rules}. */
+ public static ResultUsePolicyEvaluator create(ResultUseRule... rules) {
+ return create(Arrays.asList(rules));
+ }
+
+ /** Creates a new {@link ResultUsePolicyEvaluator} using the given {@code rules}. */
+ public static ResultUsePolicyEvaluator create(Iterable extends ResultUseRule> rules) {
+ return builder().addRules(rules).build();
+ }
+
+ /** Returns a new {@link Builder} for creating a {@link ResultUsePolicyEvaluator}. */
+ public static ResultUsePolicyEvaluator.Builder builder() {
+ return new Builder();
+ }
+
+ /** Map of method symbol kinds to the scopes that should be evaluated for that kind of symbol. */
+ private static final ImmutableListMultimap SCOPES =
+ ImmutableListMultimap.builder()
+ .putAll(ElementKind.METHOD, METHOD, ENCLOSING_ELEMENTS, GLOBAL)
+ // TODO(cgdecker): Constructors in particular (though really all methods I think) should
+ // not be able to get a policy of OPTIONAL from enclosing elements. Only defaults should
+ // come from enclosing elements, and there should only be one default policy (EXPECTED).
+ .putAll(ElementKind.CONSTRUCTOR, METHOD, ENCLOSING_ELEMENTS, GLOBAL)
+ .build();
+
+ /** All the rules for this evaluator, indexed by the scopes they apply to. */
+ private final ImmutableListMultimap rules;
+
+ private ResultUsePolicyEvaluator(Builder builder) {
+ this.rules =
+ builder.rules.stream()
+ .flatMap(rule -> rule.scopes().stream().map(scope -> entry(scope, rule)))
+ .collect(toImmutableListMultimap(Entry::getKey, Entry::getValue));
+ }
+
+ /**
+ * Evaluates the given {@code method} and returns a single {@link ResultUsePolicy} that should
+ * apply to it.
+ */
+ public ResultUsePolicy evaluate(MethodSymbol method, VisitorState state) {
+ return policies(method, state).findFirst().orElse(OPTIONAL);
+ }
+
+ private Stream policies(MethodSymbol method, VisitorState state) {
+ return SCOPES.get(method.getKind()).stream()
+ .flatMap(scope -> scope.policies(method, state, rules));
+ }
+
+ /**
+ * Returns a stream of {@link Evaluation}s made by rules starting from the given {@code method}.
+ */
+ public Stream evaluations(MethodSymbol method, VisitorState state) {
+ return SCOPES.get(method.getKind()).stream()
+ .flatMap(scope -> scope.evaluations(method, state, rules));
+ }
+
+ /** Builder for {@link ResultUsePolicyEvaluator}. */
+ public static final class Builder {
+ private final List rules = new ArrayList<>();
+
+ private Builder() {}
+
+ /** Adds the given {@code rule}. */
+ @CanIgnoreReturnValue
+ public Builder addRule(ResultUseRule rule) {
+ this.rules.add(rule);
+ return this;
+ }
+
+ /** Adds all the given {@code rules}. */
+ @CanIgnoreReturnValue
+ public Builder addRules(ResultUseRule... rules) {
+ return addRules(Arrays.asList(rules));
+ }
+
+ /** Adds all the given {@code rules}. */
+ @CanIgnoreReturnValue
+ public Builder addRules(Iterable extends ResultUseRule> rules) {
+ rules.forEach(this::addRule);
+ return this;
+ }
+
+ /** Builds a new {@link ResultUsePolicyEvaluator}. */
+ public ResultUsePolicyEvaluator build() {
+ return new ResultUsePolicyEvaluator(this);
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUseRule.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUseRule.java
new file mode 100644
index 00000000000..c7aec4f58a3
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUseRule.java
@@ -0,0 +1,191 @@
+/*
+ * 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 static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.ENCLOSING_ELEMENTS;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.GLOBAL;
+import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.RuleScope.METHOD;
+import static com.google.errorprone.util.ASTHelpers.enclosingElements;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.errorprone.VisitorState;
+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.PackageSymbol;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/** A rule for determining {@link ResultUsePolicy} for methods and/or constructors. */
+public abstract class ResultUseRule {
+
+ // TODO(cgdecker): Switching to a model where scopes can only either be in a "marked" or
+ // "unmarked" state and only methods can have a specific policy will simplify all of this a lot.
+
+ private ResultUseRule() {} // only allow the subclasses below
+
+ /** An ID for uniquely identifying this rule. */
+ public abstract String id();
+
+ /** The scopes this rule applies to. */
+ public abstract ImmutableSet scopes();
+
+ /** Evaluates the given {@code symbol} and optionally returns a {@link ResultUsePolicy} for it. */
+ public abstract Optional evaluate(Symbol symbol, VisitorState state);
+
+ /** Evaluates the given symbol and optionally returns an {@link Evaluation} of it. */
+ public final Optional evaluate(RuleScope scope, Symbol symbol, VisitorState state) {
+ return evaluate(symbol, state).map(policy -> Evaluation.create(this, scope, symbol, policy));
+ }
+
+ @Override
+ public final String toString() {
+ return id();
+ }
+
+ /**
+ * A rule that evaluates methods and constructors to determine a {@link ResultUsePolicy} for them.
+ */
+ public abstract static class MethodRule extends ResultUseRule {
+ private static final ImmutableSet SCOPES = ImmutableSet.of(METHOD);
+
+ @Override
+ public final ImmutableSet scopes() {
+ return SCOPES;
+ }
+
+ /**
+ * Evaluates the given {@code method} and optionally returns a {@link ResultUsePolicy} for it.
+ */
+ public abstract Optional evaluateMethod(
+ MethodSymbol method, VisitorState state);
+
+ @Override
+ public final Optional evaluate(Symbol symbol, VisitorState state) {
+ return symbol instanceof MethodSymbol
+ ? evaluateMethod((MethodSymbol) symbol, state)
+ : Optional.empty();
+ }
+ }
+
+ /**
+ * A rule that evaluates symbols of any kind to determine a {@link ResultUsePolicy} to associate
+ * with them.
+ */
+ public abstract static class SymbolRule extends ResultUseRule {
+ private static final ImmutableSet SCOPES =
+ ImmutableSet.of(METHOD, ENCLOSING_ELEMENTS);
+
+ @Override
+ public final ImmutableSet scopes() {
+ return SCOPES;
+ }
+ }
+
+ /**
+ * A global rule that is evaluated when none of the more specific rules determine a {@link
+ * ResultUsePolicy} for a method.
+ */
+ public abstract static class GlobalRule extends ResultUseRule {
+ private static final ImmutableSet SCOPES = ImmutableSet.of(GLOBAL);
+
+ @Override
+ public final ImmutableSet scopes() {
+ return SCOPES;
+ }
+
+ /** Optionally returns a global policy for methods or constructors. */
+ public abstract Optional evaluate(boolean constructor, VisitorState state);
+
+ @Override
+ public final Optional evaluate(Symbol symbol, VisitorState state) {
+ return evaluate(symbol.isConstructor(), state);
+ }
+ }
+
+ /** Scope to which a rule may apply. */
+ public enum RuleScope {
+ /** The specific method or constructor for which a {@link ResultUsePolicy} is being chosen. */
+ METHOD {
+ @Override
+ Stream members(MethodSymbol method) {
+ return Stream.of(method);
+ }
+ },
+ /**
+ * Classes and package that enclose a method for which a {@link ResultUsePolicy} is being
+ * chosen.
+ */
+ ENCLOSING_ELEMENTS {
+ @Override
+ Stream members(MethodSymbol method) {
+ return enclosingElements(method)
+ .filter(s -> s instanceof ClassSymbol || s instanceof PackageSymbol);
+ }
+ },
+ /** The global scope. */
+ GLOBAL {
+ @Override
+ Stream members(MethodSymbol method) {
+ return Stream.of(method);
+ }
+ };
+
+ /** Returns an ordered stream of elements in this scope relative to the given {@code method}. */
+ abstract Stream members(MethodSymbol method);
+
+ /** Returns an ordered stream of policies from rules in this scope. */
+ final Stream policies(
+ MethodSymbol method, VisitorState state, ListMultimap rules) {
+ List scopeRules = rules.get(this);
+ return members(method)
+ .flatMap(symbol -> scopeRules.stream().map(rule -> rule.evaluate(symbol, state)))
+ .flatMap(Optional::stream);
+ }
+
+ /** Returns an ordered stream of evaluations in this scope. */
+ final Stream evaluations(
+ MethodSymbol method, VisitorState state, ListMultimap rules) {
+ List scopeRules = rules.get(this);
+ return members(method)
+ .flatMap(symbol -> scopeRules.stream().map(rule -> rule.evaluate(this, symbol, state)))
+ .flatMap(Optional::stream);
+ }
+ }
+
+ /** An evaluation that a rule makes. */
+ @AutoValue
+ public abstract static class Evaluation {
+ /** Creates a new {@link Evaluation}. */
+ public static Evaluation create(
+ ResultUseRule rule, RuleScope scope, Symbol element, ResultUsePolicy policy) {
+ return new AutoValue_ResultUseRule_Evaluation(rule, scope, element, policy);
+ }
+
+ /** The rule that made this evaluation. */
+ public abstract ResultUseRule rule();
+ /** The scope at which the evaluation was made. */
+ public abstract RuleScope scope();
+ /** The specific element in the scope for which the evaluation was made. */
+ public abstract Symbol element();
+ /** The policy the rule selected. */
+ public abstract ResultUsePolicy policy();
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Rules.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Rules.java
new file mode 100644
index 00000000000..8889fc20411
--- /dev/null
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Rules.java
@@ -0,0 +1,101 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName;
+
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.GlobalRule;
+import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.SymbolRule;
+import com.sun.tools.javac.code.Symbol;
+import java.util.Optional;
+import java.util.function.BiPredicate;
+
+/** Factories for common kinds {@link ResultUseRule}s. */
+public final class Rules {
+
+ private Rules() {}
+
+ /**
+ * Returns a simple global rule that always returns the given defaults for methods and
+ * constructors.
+ */
+ public static ResultUseRule globalDefault(
+ Optional methodDefault, Optional constructorDefault) {
+ return new SimpleGlobalRule("GLOBAL_DEFAULT", methodDefault, constructorDefault);
+ }
+
+ /**
+ * Returns a {@link ResultUseRule} that maps annotations with the given {@code simpleName} to the
+ * given {@code policy}.
+ */
+ public static ResultUseRule mapAnnotationSimpleName(String simpleName, ResultUsePolicy policy) {
+ return new SimpleRule(
+ "ANNOTATION @" + simpleName,
+ (sym, st) -> hasDirectAnnotationWithSimpleName(sym, simpleName),
+ policy);
+ }
+
+ private static final class SimpleRule extends SymbolRule {
+ private final String name;
+ private final BiPredicate predicate;
+ private final ResultUsePolicy policy;
+
+ private SimpleRule(
+ String name, BiPredicate predicate, ResultUsePolicy policy) {
+ this.name = name;
+ this.predicate = predicate;
+ this.policy = policy;
+ }
+
+ @Override
+ public String id() {
+ return name;
+ }
+
+ @Override
+ public Optional evaluate(Symbol symbol, VisitorState state) {
+ return predicate.test(symbol, state) ? Optional.of(policy) : Optional.empty();
+ }
+ }
+
+ private static final class SimpleGlobalRule extends GlobalRule {
+ private final String id;
+ private final Optional methodDefault;
+ private final Optional constructorDefault;
+
+ private SimpleGlobalRule(
+ String id,
+ Optional methodDefault,
+ Optional constructorDefault) {
+ this.id = checkNotNull(id);
+ this.methodDefault = methodDefault;
+ this.constructorDefault = constructorDefault;
+ }
+
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public Optional evaluate(boolean constructor, VisitorState state) {
+ return constructor ? constructorDefault : methodDefault;
+ }
+ }
+}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/PrivateConstructorForNoninstantiableModule.java b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/PrivateConstructorForNoninstantiableModule.java
index eb60ab62465..37ea935d042 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/PrivateConstructorForNoninstantiableModule.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/PrivateConstructorForNoninstantiableModule.java
@@ -15,6 +15,7 @@
*/
package com.google.errorprone.bugpatterns.inject.dagger;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.isBindingDeclarationMethod;
import static com.google.errorprone.matchers.Description.NO_MATCH;
@@ -25,9 +26,7 @@
import static com.sun.source.tree.Tree.Kind.CLASS;
import static com.sun.source.tree.Tree.Kind.METHOD;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -39,6 +38,7 @@
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
+import java.util.function.Predicate;
/**
* @author gak@google.com (Gregory Kick)
@@ -48,14 +48,6 @@
severity = SUGGESTION)
public class PrivateConstructorForNoninstantiableModule extends BugChecker
implements ClassTreeMatcher {
- private static final Predicate IS_CONSTRUCTOR =
- new Predicate() {
- @Override
- public boolean apply(Tree tree) {
- return getSymbol(tree).isConstructor();
- }
- };
-
@Override
public Description matchClass(ClassTree classTree, VisitorState state) {
if (!DaggerAnnotations.isAnyModule().matches(classTree, state)) {
@@ -67,35 +59,32 @@ public Description matchClass(ClassTree classTree, VisitorState state) {
return NO_MATCH;
}
- FluentIterable extends Tree> nonSyntheticMembers =
- FluentIterable.from(classTree.getMembers())
+ ImmutableList nonSyntheticMembers =
+ classTree.getMembers().stream()
.filter(
- Predicates.not(
- new Predicate() {
- @Override
- public boolean apply(Tree tree) {
- return tree.getKind().equals(METHOD)
- && isGeneratedConstructor((MethodTree) tree);
- }
- }));
+ tree ->
+ !(tree.getKind().equals(METHOD) && isGeneratedConstructor((MethodTree) tree)))
+ .collect(toImmutableList());
// ignore empty modules
if (nonSyntheticMembers.isEmpty()) {
return NO_MATCH;
}
- if (nonSyntheticMembers.anyMatch(IS_CONSTRUCTOR)) {
+ if (nonSyntheticMembers.stream().anyMatch(tree -> getSymbol(tree).isConstructor())) {
return NO_MATCH;
}
boolean hasBindingDeclarationMethods =
- nonSyntheticMembers.anyMatch(matcherAsPredicate(isBindingDeclarationMethod(), state));
+ nonSyntheticMembers.stream()
+ .anyMatch(matcherAsPredicate(isBindingDeclarationMethod(), state));
if (hasBindingDeclarationMethods) {
return describeMatch(classTree, addPrivateConstructor(classTree, state));
}
- boolean allStaticMembers = nonSyntheticMembers.allMatch(matcherAsPredicate(isStatic(), state));
+ boolean allStaticMembers =
+ nonSyntheticMembers.stream().allMatch(matcherAsPredicate(isStatic(), state));
if (allStaticMembers) {
return describeMatch(classTree, addPrivateConstructor(classTree, state));
@@ -110,11 +99,6 @@ private static Fix addPrivateConstructor(ClassTree classTree, VisitorState state
private static Predicate matcherAsPredicate(
Matcher super T> matcher, VisitorState state) {
- return new Predicate() {
- @Override
- public boolean apply(T t) {
- return matcher.matches(t, state);
- }
- };
+ return t -> matcher.matches(t, state);
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/RefersToDaggerCodegen.java b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/RefersToDaggerCodegen.java
index 6ab355c7834..efd00e5b4cd 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/RefersToDaggerCodegen.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/RefersToDaggerCodegen.java
@@ -16,6 +16,7 @@
package com.google.errorprone.bugpatterns.inject.dagger;
+import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.getGeneratedBy;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
@@ -97,7 +98,8 @@ private static boolean isGeneratedBaseType(
}
private static boolean isDaggerInternalClass(ClassSymbol symbol) {
- return DAGGER_INTERNAL_PACKAGES.contains(symbol.packge().getQualifiedName().toString());
+ return DAGGER_INTERNAL_PACKAGES.contains(
+ enclosingPackage(symbol).getQualifiedName().toString());
}
private static boolean isAllowedToReferenceDaggerInternals(VisitorState state) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullable.java
index f4bd41f18bc..dcb066b9133 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullable.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullable.java
@@ -19,16 +19,18 @@
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToType;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isAlreadyAnnotatedNullable;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isInNullMarkedScope;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.equalsMethodDeclaration;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import com.google.errorprone.BugPattern;
+import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
-import com.google.errorprone.dataflow.nullnesspropagation.Nullness;
-import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.MethodTree;
@@ -40,15 +42,29 @@
summary = "Method overrides Object.equals but does not have @Nullable on its parameter",
severity = SUGGESTION)
public class EqualsMissingNullable extends BugChecker implements MethodTreeMatcher {
+ private final boolean beingConservative;
+
+ public EqualsMissingNullable(ErrorProneFlags flags) {
+ this.beingConservative = nullnessChecksShouldBeConservative(flags);
+ }
+
@Override
public Description matchMethod(MethodTree methodTree, VisitorState state) {
+ if (beingConservative && state.errorProneOptions().isTestOnlyTarget()) {
+ return NO_MATCH;
+ }
+
if (!equalsMethodDeclaration().matches(methodTree, state)) {
return NO_MATCH;
}
VariableTree parameterTree = getOnlyElement(methodTree.getParameters());
VarSymbol parameter = getSymbol(parameterTree);
- if (NullnessAnnotations.fromAnnotationsOn(parameter).orElse(null) == Nullness.NULLABLE) {
+ if (isAlreadyAnnotatedNullable(parameter)) {
+ return NO_MATCH;
+ }
+
+ if (beingConservative && !isInNullMarkedScope(parameter, state)) {
return NO_MATCH;
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullable.java
index 46f7f800368..2187202b850 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullable.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullable.java
@@ -21,6 +21,8 @@
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToType;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.getNullCheck;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isAlreadyAnnotatedNullable;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.varsProvenNullByParentIf;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
@@ -35,8 +37,6 @@
import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.bugpatterns.nullness.NullnessUtils.NullCheck;
-import com.google.errorprone.dataflow.nullnesspropagation.Nullness;
-import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.AssignmentTree;
@@ -57,7 +57,7 @@ public class FieldMissingNullable extends BugChecker
private final boolean beingConservative;
public FieldMissingNullable(ErrorProneFlags flags) {
- this.beingConservative = flags.getBoolean("Nullness:Conservative").orElse(true);
+ this.beingConservative = nullnessChecksShouldBeConservative(flags);
}
@Override
@@ -130,7 +130,7 @@ private Description matchIfLocallyDeclaredReferenceFieldWithoutNullable(
* *too* conservative, even for ReturnMissingNullable.)
*/
- if (NullnessAnnotations.fromAnnotationsOn(assigned).orElse(null) == Nullness.NULLABLE) {
+ if (isAlreadyAnnotatedNullable(assigned)) {
return NO_MATCH;
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java
index f65957ec357..6dff9193333 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullnessUtils.java
@@ -26,6 +26,7 @@
import static com.google.errorprone.suppliers.Suppliers.JAVA_LANG_VOID_TYPE;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
+import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
import static com.google.errorprone.util.ASTHelpers.stripParentheses;
import static com.sun.source.tree.Tree.Kind.ARRAY_TYPE;
import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
@@ -37,6 +38,8 @@
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.nullness.NullnessUtils.NullCheck.Polarity;
+import com.google.errorprone.dataflow.nullnesspropagation.Nullness;
+import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Matcher;
@@ -84,6 +87,42 @@ private NullnessUtils() {}
private static final Matcher OPTIONAL_OR_ELSE =
instanceMethod().onDescendantOf("java.util.Optional").named("orElse");
+ /**
+ * Returns {@code true} if the flags request that we look to add @Nullable annotations only where
+ * they are nearly certain to be correct and to be about as uncontroversial as nullness
+ * annotations can ever be. In Google terms, that means annotations that we'd be willing to roll
+ * out across the depot with global approval.
+ *
+ *
If this method returns {@code false}, that gives checkers permission to be more aggressive.
+ * Their suggestions should still be very likely to be correct, but the goal is more to assist a
+ * human who is aiming to annotate a codebase. The expectation, then, is that at least one human
+ * will check whether each new annotation is justified.
+ */
+ static boolean nullnessChecksShouldBeConservative(ErrorProneFlags flags) {
+ return flags.getBoolean("Nullness:Conservative").orElse(true);
+ }
+
+ /*
+ * TODO(cpovirk): Walking up the tree of enclosing elements may be more expensive than we'd like.
+ * (But I haven't checked.) To improve upon that, would we go so far as to build special tracking
+ * of @NullMarked-ness of the current TreePath into Error Prone itself? (Of course, even that
+ * would help only with the case in which we're interested in the @NullMarked-ness of the tree
+ * we're currently visiting.)
+ *
+ * Another advantage of that approach is that callers wouldn't need to start from a Symbol. For
+ * example, VoidMissingNullable.matchParameterizedType wouldn't have to walk up the path to find
+ * such a Symbol.
+ */
+
+ static boolean isInNullMarkedScope(Symbol sym, VisitorState state) {
+ for (; sym != null; sym = sym.getEnclosingElement()) {
+ if (hasAnnotation(sym, "org.jspecify.nullness.NullMarked", state)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a {@link SuggestedFix} to add a {@code Nullable} annotation to the given method's
* return type.
@@ -188,6 +227,10 @@ private static SuggestedFix fixByAddingKnownTypeUseNullableAnnotation(
// TODO(cpovirk): Remove any @NonNull, etc. annotation that is present?
}
+ static boolean isAlreadyAnnotatedNullable(Symbol symbol) {
+ return NullnessAnnotations.fromAnnotationsOn(symbol).orElse(null) == Nullness.NULLABLE;
+ }
+
@com.google.auto.value.AutoValue // fully qualified to work around JDK-7177813(?) in JDK8 build
abstract static class NullableAnnotationToUse {
static NullableAnnotationToUse annotationToBeImported(String qualifiedName, boolean isTypeUse) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java
index c12a9e2d37c..ced1b26104b 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullable.java
@@ -16,26 +16,36 @@
package com.google.errorprone.bugpatterns.nullness;
+import static com.google.common.collect.Streams.forEachPair;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.findDeclaration;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToType;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.getNullCheck;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isAlreadyAnnotatedNullable;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative;
import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static com.google.errorprone.util.ASTHelpers.enclosingClass;
+import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.hasNoExplicitType;
import static javax.lang.model.element.ElementKind.PARAMETER;
+import static javax.lang.model.type.TypeKind.TYPEVAR;
+import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
+import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
import com.google.errorprone.bugpatterns.nullness.NullnessUtils.NullCheck;
-import com.google.errorprone.dataflow.nullnesspropagation.Nullness;
-import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
@@ -45,14 +55,29 @@
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
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.List;
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
@BugPattern(
summary = "Parameter has handling for null but is not annotated @Nullable",
severity = SUGGESTION)
-public class ParameterMissingNullable extends BugChecker implements BinaryTreeMatcher {
+public final class ParameterMissingNullable extends BugChecker
+ implements BinaryTreeMatcher, MethodInvocationTreeMatcher, NewClassTreeMatcher {
+ private final boolean beingConservative;
+
+ public ParameterMissingNullable(ErrorProneFlags flags) {
+ this.beingConservative = nullnessChecksShouldBeConservative(flags);
+ }
+
@Override
public Description matchBinary(BinaryTree tree, VisitorState state) {
+ if (beingConservative) {
+ // The rules in matchBinary are mostly heuristics, as discussed in the large comment below.
+ return NO_MATCH;
+ }
+
/*
* This check's basic principle is: If an implementation checks `param == null` or
* `param != null`, then it's going to take one of two actions:
@@ -153,9 +178,7 @@ private static boolean isLoopCondition(TreePath path) {
}
private static boolean isParameterWithoutNullable(Symbol sym) {
- return sym != null
- && sym.getKind() == PARAMETER
- && NullnessAnnotations.fromAnnotationsOn(sym).orElse(null) != Nullness.NULLABLE;
+ return sym != null && sym.getKind() == PARAMETER && !isAlreadyAnnotatedNullable(sym);
}
private static boolean nullCheckLikelyToProduceException(VisitorState state) {
@@ -208,6 +231,104 @@ public Void visitThrow(ThrowTree tree, Void unused) {
return likelyToProduceException[0];
}
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ return matchCall(getSymbol(tree), tree.getArguments(), state);
+ }
+
+ @Override
+ public Description matchNewClass(NewClassTree tree, VisitorState state) {
+ return matchCall(getSymbol(tree), tree.getArguments(), state);
+ }
+
+ private static boolean hasExtraParameterForEnclosingInstance(MethodSymbol symbol) {
+ if (!symbol.isConstructor()) {
+ return false;
+ }
+ ClassSymbol constructedClass = enclosingClass(symbol);
+ return enclosingClass(constructedClass) != null && !constructedClass.isStatic();
+ }
+
+ private Description matchCall(
+ MethodSymbol methodSymbol, List extends ExpressionTree> arguments, VisitorState state) {
+ if (hasExtraParameterForEnclosingInstance(methodSymbol)) {
+ // TODO(cpovirk): Figure out the right way to handle the implicit outer `this` parameter.
+ return NO_MATCH;
+ }
+
+ if (methodSymbol.isVarArgs()) {
+ /*
+ * TODO(cpovirk): Figure out the right way to handle this, or at least handle all parameters
+ * but the last.
+ */
+ return NO_MATCH;
+ }
+
+ forEachPair(
+ arguments.stream(),
+ methodSymbol.getParameters().stream(),
+ (argTree, paramSymbol) -> {
+ if (!hasDefinitelyNullBranch(
+ argTree,
+ /*
+ * TODO(cpovirk): Precompute sets of definitelyNullVars and varsProvenNullByParentIf
+ * instead of passing empty sets.
+ */
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ state)) {
+ return;
+ }
+
+ if (isAlreadyAnnotatedNullable(paramSymbol)) {
+ return;
+ }
+
+ if (paramSymbol.asType().getKind() == TYPEVAR) {
+ // TODO(cpovirk): Don't always give up for type variables, at least in aggressive mode.
+ return;
+ }
+
+ VariableTree paramTree = findDeclaration(state, paramSymbol);
+ if (paramTree == null) {
+ /*
+ * First, we can't reliably make changes to declarations in other compilation units.
+ *
+ * But even if we could, we'd "trust" calls in other compilation units less: Plenty of
+ * code passes null when it "shouldn't":
+ *
+ * - Some code gets away with it because that call never runs.
+ *
+ * - Some tests get away with it because they know that no one will read the value.
+ *
+ * - Some tests are deliberately checking that passing null produces NPE.
+ *
+ * Still, maybe we'd consider trusting such calls when running in aggressive mode if we
+ * had the ability someday.
+ */
+ return;
+ }
+
+ SuggestedFix fix = fixByAddingNullableAnnotationToType(state, paramTree);
+ if (fix.isEmpty()) {
+ return;
+ }
+
+ /*
+ * TODO(cpovirk): Would it be better to report this on the parameter, rather than the
+ * argument? If so, we may want to rework this checker to be a CompilationUnitMatcher.
+ * That way, it can scan the whole file to find parameters and *then* evaluate
+ * suppressions. (Under the current MethodInvocationTreeMatcher approach, a suppression at
+ * the *arg* site would suppress errors that would be reported on the param. Even if we
+ * were to manually make suppressions at the param *also* have an effect, the remaining
+ * effect for *arg*-site suppressions would be unfortunate.)
+ */
+ state.reportMatch(describeMatch(argTree, fix));
+ });
+
+ return NO_MATCH;
+ }
+
/*
* TODO(cpovirk): Check for assignment to a @Nullable field. We'll need special cases, though:
*
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java
index 111e9981faa..861fb26e260 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullable.java
@@ -23,7 +23,9 @@
import static com.google.errorprone.VisitorState.memoize;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToReturnType;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isAlreadyAnnotatedNullable;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isVoid;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.varsProvenNullByParentIf;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.anyMethod;
@@ -46,8 +48,6 @@
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
-import com.google.errorprone.dataflow.nullnesspropagation.Nullness;
-import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
@@ -185,7 +185,7 @@ public class ReturnMissingNullable extends BugChecker implements CompilationUnit
private final boolean beingConservative;
public ReturnMissingNullable(ErrorProneFlags flags) {
- this.beingConservative = flags.getBoolean("Nullness:Conservative").orElse(true);
+ this.beingConservative = nullnessChecksShouldBeConservative(flags);
}
@Override
@@ -268,8 +268,7 @@ void doVisitMethod(MethodTree tree) {
* codebase).
*/
- if (NullnessAnnotations.fromAnnotationsOn(possibleOverride).orElse(null)
- == Nullness.NULLABLE) {
+ if (isAlreadyAnnotatedNullable(possibleOverride)) {
return;
}
@@ -346,7 +345,7 @@ && methodCanBeOverridden(method)) {
return;
}
- if (NullnessAnnotations.fromAnnotationsOn(method).orElse(null) == Nullness.NULLABLE) {
+ if (isAlreadyAnnotatedNullable(method)) {
return;
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java
index f5dadd22d11..629746f5de2 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullable.java
@@ -22,14 +22,18 @@
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAddingNullableAnnotationToType;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.fixByAnnotatingTypeUseOnlyLocationWithNullableAnnotation;
import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.isVoid;
+import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.hasNoExplicitType;
+import static com.sun.source.tree.Tree.Kind.METHOD;
import static javax.lang.model.element.ElementKind.LOCAL_VARIABLE;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
+import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
@@ -41,6 +45,7 @@
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
@@ -55,6 +60,12 @@
@BugPattern(summary = "The type Void is not annotated @Nullable", severity = SUGGESTION)
public class VoidMissingNullable extends BugChecker
implements ParameterizedTypeTreeMatcher, MethodTreeMatcher, VariableTreeMatcher {
+ private final boolean beingConservative;
+
+ public VoidMissingNullable(ErrorProneFlags flags) {
+ this.beingConservative = nullnessChecksShouldBeConservative(flags);
+ }
+
/*
* TODO(cpovirk): Handle `Void[]`, probably mostly in casts, while avoiding `Void[].class`.
*
@@ -73,6 +84,14 @@ public class VoidMissingNullable extends BugChecker
@Override
public Description matchParameterizedType(
ParameterizedTypeTree parameterizedTypeTree, VisitorState state) {
+ if (beingConservative && state.errorProneOptions().isTestOnlyTarget()) {
+ return NO_MATCH;
+ }
+
+ if (beingConservative && !isInNullMarkedScope(state)) {
+ return NO_MATCH;
+ }
+
for (Tree tree : parameterizedTypeTree.getTypeArguments()) {
if (tree instanceof WildcardTree) {
tree = ((WildcardTree) tree).getBound();
@@ -82,17 +101,47 @@ public Description matchParameterizedType(
return NO_MATCH; // Any reports were made through state.reportMatch.
}
+ /*
+ * TODO(cpovirk): Consider promoting this variant of isInNullMarkedScope to live in NullnessUtils
+ * alongside the main variant. But note that it may be even more expensive than the main variant,
+ * and see the more ambitious alternative TODO(cpovirk): in that file.
+ */
+ private static boolean isInNullMarkedScope(VisitorState state) {
+ for (Tree tree : state.getPath()) {
+ if (tree.getKind().asInterface().equals(ClassTree.class) || tree.getKind() == METHOD) {
+ Symbol enclosingElement = getSymbol(tree);
+ if (tree == null) {
+ continue;
+ }
+ return NullnessUtils.isInNullMarkedScope(enclosingElement, state);
+ }
+ }
+ throw new AssertionError(
+ "parameterized type without enclosing element: " + Iterables.toString(state.getPath()));
+ }
+
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
+ if (beingConservative && state.errorProneOptions().isTestOnlyTarget()) {
+ return NO_MATCH;
+ }
+
MethodSymbol sym = getSymbol(tree);
if (!typeMatches(sym.getReturnType(), sym, state)) {
return NO_MATCH;
}
+ if (beingConservative && !NullnessUtils.isInNullMarkedScope(sym, state)) {
+ return NO_MATCH;
+ }
return describeMatch(tree, fixByAddingNullableAnnotationToReturnType(state, tree));
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
+ if (beingConservative && state.errorProneOptions().isTestOnlyTarget()) {
+ return NO_MATCH;
+ }
+
if (hasNoExplicitType(tree, state)) {
/*
* In the case of `var`, a declaration-annotation @Nullable would be valid. But a type-use
@@ -109,6 +158,9 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
if (!typeMatches(sym.type, sym, state)) {
return NO_MATCH;
}
+ if (beingConservative && !NullnessUtils.isInNullMarkedScope(sym, state)) {
+ return NO_MATCH;
+ }
SuggestedFix fix = fixByAddingNullableAnnotationToType(state, tree);
if (fix.isEmpty()) {
return NO_MATCH;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/AbstractLockMethodChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/AbstractLockMethodChecker.java
deleted file mode 100644
index 649bf1d3766..00000000000
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/AbstractLockMethodChecker.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2014 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.threadsafety;
-
-import com.google.common.base.Functions;
-import com.google.common.base.Joiner;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Sets.SetView;
-import com.google.errorprone.VisitorState;
-import com.google.errorprone.bugpatterns.BugChecker;
-import com.google.errorprone.matchers.Description;
-import com.sun.source.tree.MethodTree;
-import com.sun.source.tree.Tree;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Abstract implementation of checkers for {@code @LockMethod} and{@code @UnlockMethod}.
- *
- * @author cushon@google.com (Liam Miller-Cushon)
- */
-public abstract class AbstractLockMethodChecker extends BugChecker
- implements BugChecker.MethodTreeMatcher {
-
- /**
- * Returns the lock expressions in the {@code @LockMethod}/{@code @UnlockMethod} annotation, if
- * any.
- */
- protected abstract ImmutableList getLockExpressions(MethodTree tree);
-
- /** Searches the method body for locks that are acquired/released. */
- protected abstract Set getActual(MethodTree tree, VisitorState state);
-
- /**
- * Searches the method body for the incorrect lock operation (e.g. releasing a lock in
- * {@code @LockMethod}, or acquiring a lock in {@code @UnlockMethod}).
- */
- protected abstract Set getUnwanted(MethodTree tree, VisitorState state);
-
- /** Builds the error message, given the list of locks that were not handled. */
- protected abstract String buildMessage(String unhandled);
-
- @Override
- public Description matchMethod(MethodTree tree, VisitorState state) {
-
- ImmutableList lockExpressions = getLockExpressions(tree);
- if (lockExpressions.isEmpty()) {
- return Description.NO_MATCH;
- }
-
- Optional> expected =
- parseLockExpressions(lockExpressions, tree, state);
- if (!expected.isPresent()) {
- return buildDescription(tree).setMessage("Could not resolve lock expression.").build();
- }
-
- Set unwanted = getUnwanted(tree, state);
- SetView mishandled = Sets.intersection(expected.get(), unwanted);
- if (!mishandled.isEmpty()) {
- String message = buildMessage(formatLockString(mishandled));
- return buildDescription(tree).setMessage(message).build();
- }
-
- Set actual = getActual(tree, state);
- SetView unhandled = Sets.difference(expected.get(), actual);
- if (!unhandled.isEmpty()) {
- String message = buildMessage(formatLockString(unhandled));
- return buildDescription(tree).setMessage(message).build();
- }
-
- return Description.NO_MATCH;
- }
-
- private static String formatLockString(Set locks) {
- ImmutableList sortedUnhandled =
- FluentIterable.from(locks)
- .transform(Functions.toStringFunction())
- .toSortedList(Comparator.naturalOrder());
- return Joiner.on(", ").join(sortedUnhandled);
- }
-
- private static Optional> parseLockExpressions(
- List lockExpressions, Tree tree, VisitorState state) {
- ImmutableSet.Builder builder = ImmutableSet.builder();
- for (String lockExpression : lockExpressions) {
- Optional guard =
- GuardedByBinder.bindString(
- lockExpression, GuardedBySymbolResolver.from(tree, state), GuardedByFlags.allOn());
- if (!guard.isPresent()) {
- return Optional.empty();
- }
- builder.add(guard.get());
- }
- return Optional.of(builder.build());
- }
-}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java
index cbed17f6986..55900c9057d 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByChecker.java
@@ -56,10 +56,13 @@ public class GuardedByChecker extends BugChecker
private final GuardedByFlags flags = GuardedByFlags.allOn();
private final boolean reportMissingGuards;
+ private final boolean checkTryWithResources;
public GuardedByChecker(ErrorProneFlags errorProneFlags) {
reportMissingGuards =
errorProneFlags.getBoolean("GuardedByChecker:reportMissingGuards").orElse(true);
+ checkTryWithResources =
+ errorProneFlags.getBoolean("GuardedByChecker:checkTryWithResources").orElse(true);
}
@Override
@@ -87,7 +90,8 @@ private void analyze(VisitorState state) {
report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state),
tree1 -> isSuppressed(tree1, state),
flags,
- reportMissingGuards);
+ reportMissingGuards,
+ checkTryWithResources);
}
@Override
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java
index 52d98bc3cdf..1daff1c5725 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java
@@ -19,7 +19,6 @@
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.errorprone.VisitorState;
@@ -54,6 +53,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Predicate;
import javax.lang.model.element.Modifier;
/**
@@ -86,10 +86,12 @@ public static void analyze(
LockEventListener listener,
Predicate isSuppressed,
GuardedByFlags flags,
- boolean reportMissingGuards) {
+ boolean reportMissingGuards,
+ boolean checkTryWithResources) {
HeldLockSet locks = HeldLockSet.empty();
locks = handleMonitorGuards(state, locks, flags);
- new LockScanner(state, listener, isSuppressed, flags, reportMissingGuards)
+ new LockScanner(
+ state, listener, isSuppressed, flags, reportMissingGuards, checkTryWithResources)
.scan(state.getPath(), locks);
}
@@ -126,6 +128,7 @@ private static class LockScanner extends TreePathScanner {
private final Predicate isSuppressed;
private final GuardedByFlags flags;
private final boolean reportMissingGuards;
+ private final boolean checkTryWithResources;
private static final GuardedByExpression.Factory F = new GuardedByExpression.Factory();
@@ -134,17 +137,19 @@ private LockScanner(
LockEventListener listener,
Predicate isSuppressed,
GuardedByFlags flags,
- boolean reportMissingGuards) {
+ boolean reportMissingGuards,
+ boolean checkTryWithResources) {
this.visitorState = visitorState;
this.listener = listener;
this.isSuppressed = isSuppressed;
this.flags = flags;
this.reportMissingGuards = reportMissingGuards;
+ this.checkTryWithResources = checkTryWithResources;
}
@Override
public Void visitMethod(MethodTree tree, HeldLockSet locks) {
- if (isSuppressed.apply(tree)) {
+ if (isSuppressed.test(tree)) {
return null;
}
// Synchronized instance methods hold the 'this' lock; synchronized static methods
@@ -182,12 +187,11 @@ public Void visitTry(TryTree tree, HeldLockSet locks) {
// are held for the entirety of the try and catch statements.
Collection releasedLocks =
ReleasedLockFinder.find(tree.getFinallyBlock(), visitorState, flags);
- if (resources.isEmpty()) {
+ // We don't know what to do with the try-with-resources block.
+ // TODO(cushon) - recognize common try-with-resources patterns. Currently there is no
+ // standard implementation of an AutoCloseable lock resource to detect.
+ if (checkTryWithResources || resources.isEmpty()) {
scan(tree.getBlock(), locks.plusAll(releasedLocks));
- } else {
- // We don't know what to do with the try-with-resources block.
- // TODO(cushon) - recognize common try-with-resources patterns. Currently there is no
- // standard implementation of an AutoCloseable lock resource to detect.
}
scan(tree.getCatches(), locks.plusAll(releasedLocks));
scan(tree.getFinallyBlock(), locks);
@@ -234,12 +238,12 @@ public Void visitLambdaExpression(LambdaExpressionTree node, HeldLockSet heldLoc
@Override
public Void visitVariable(VariableTree node, HeldLockSet locks) {
- return isSuppressed.apply(node) ? null : super.visitVariable(node, locks);
+ return isSuppressed.test(node) ? null : super.visitVariable(node, locks);
}
@Override
public Void visitClass(ClassTree node, HeldLockSet locks) {
- return isSuppressed.apply(node) ? null : super.visitClass(node, locks);
+ return isSuppressed.test(node) ? null : super.visitClass(node, locks);
}
private void checkMatch(ExpressionTree tree, HeldLockSet locks) {
@@ -387,17 +391,9 @@ private void handleUnlockAnnotatedMethods(MethodInvocationTree tree) {
return;
}
for (String lockString : annotation.value()) {
- Optional guard =
- GuardedByBinder.bindString(
- lockString, GuardedBySymbolResolver.from(tree, state), flags);
- // TODO(cushon): http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#ifPresent
- if (guard.isPresent()) {
- Optional lock =
- ExpectedLockCalculator.from((JCExpression) tree, guard.get(), state, flags);
- if (lock.isPresent()) {
- locks.add(lock.get());
- }
- }
+ GuardedByBinder.bindString(lockString, GuardedBySymbolResolver.from(tree, state), flags)
+ .flatMap(guard -> ExpectedLockCalculator.from((JCExpression) tree, guard, state, flags))
+ .ifPresent(locks::add);
}
}
}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java
index d3fdcf1dcf6..1337cb52a9a 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java
@@ -17,12 +17,18 @@
package com.google.errorprone.bugpatterns.threadsafety;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Streams.stream;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
+import static com.google.errorprone.util.ASTHelpers.getReceiverType;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
+import static com.google.errorprone.util.ASTHelpers.isLocal;
+import static com.google.errorprone.util.ASTHelpers.isSameType;
+import static com.google.errorprone.util.ASTHelpers.isSubtype;
+import static com.google.errorprone.util.ASTHelpers.targetType;
import static java.lang.String.format;
import static java.util.stream.Collectors.joining;
@@ -43,7 +49,6 @@
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
-import com.google.errorprone.bugpatterns.threadsafety.ImmutableAnalysis.ViolationReporter;
import com.google.errorprone.bugpatterns.threadsafety.ThreadSafety.Violation;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
@@ -60,6 +65,7 @@
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
@@ -95,7 +101,7 @@ public class ImmutableChecker extends BugChecker
private final WellKnownMutability wellKnownMutability;
private final ImmutableSet immutableAnnotations;
- private final boolean matchLambdas;
+ private final boolean handleAnonymousClasses;
ImmutableChecker(ImmutableSet immutableAnnotations) {
this(ErrorProneFlags.empty(), immutableAnnotations);
@@ -108,131 +114,67 @@ public ImmutableChecker(ErrorProneFlags flags) {
private ImmutableChecker(ErrorProneFlags flags, ImmutableSet immutableAnnotations) {
this.wellKnownMutability = WellKnownMutability.fromFlags(flags);
this.immutableAnnotations = immutableAnnotations;
- this.matchLambdas = flags.getBoolean("ImmutableChecker:MatchLambdas").orElse(true);
+ this.handleAnonymousClasses =
+ flags.getBoolean("ImmutableChecker:HandleAnonymousClasses").orElse(true);
}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
- if (!matchLambdas) {
- return NO_MATCH;
- }
TypeSymbol lambdaType = getType(tree).tsym;
+ ImmutableAnalysis analysis = createImmutableAnalysis(state);
+ Violation info =
+ analysis.checkInstantiation(
+ lambdaType.getTypeParameters(), getType(tree).getTypeArguments());
+
+ if (info.isPresent()) {
+ state.reportMatch(buildDescription(tree).setMessage(info.message()).build());
+ }
if (!hasImmutableAnnotation(lambdaType, state)) {
return NO_MATCH;
}
- Set variablesClosed = new HashSet<>();
- SetMultimap typesClosed = LinkedHashMultimap.create();
- Set variablesOwnedByLambda = new HashSet<>();
-
- new TreePathScanner() {
- @Override
- public Void visitVariable(VariableTree tree, Void unused) {
- var symbol = getSymbol(tree);
- variablesOwnedByLambda.add(symbol);
- return super.visitVariable(tree, null);
- }
-
- @Override
- public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
- if (getReceiver(tree) == null) {
- var symbol = getSymbol(tree);
- if (!symbol.isStatic()) {
- // TODO(b/77333859): This isn't precise. What we really want is the type of `this`, if
- // the method call were qualified with it.
- typesClosed.put((ClassSymbol) symbol.owner, symbol);
- }
- }
- return super.visitMethodInvocation(tree, null);
- }
-
- @Override
- public Void visitMemberSelect(MemberSelectTree tree, Void unused) {
- // Special case the access of fields to allow accessing fields which would pass an immutable
- // check.
- if (tree.getExpression() instanceof IdentifierTree
- && getSymbol(tree) instanceof VarSymbol) {
- handleIdentifier(getSymbol(tree));
- // If we're only seeing a field access, don't complain about the fact we closed around
- // `this`.
- if (tree.getExpression() instanceof IdentifierTree
- && ((IdentifierTree) tree.getExpression()).getName().contentEquals("this")) {
- return null;
- }
- }
- return super.visitMemberSelect(tree, null);
- }
-
- @Override
- public Void visitIdentifier(IdentifierTree tree, Void unused) {
- handleIdentifier(getSymbol(tree));
- return super.visitIdentifier(tree, null);
- }
-
- private void handleIdentifier(Symbol symbol) {
- if (symbol instanceof VarSymbol && !variablesOwnedByLambda.contains(symbol)) {
- variablesClosed.add((VarSymbol) symbol);
- }
- }
- }.scan(state.getPath(), null);
-
- ImmutableAnalysis analysis = createImmutableAnalysis(state);
- ImmutableSet typarams =
- immutableTypeParametersInScope(getSymbol(tree), state, analysis);
- variablesClosed.stream()
- .map(closedVariable -> checkClosedLambdaVariable(closedVariable, tree, typarams, analysis))
- .filter(Violation::isPresent)
- .forEachOrdered(
- v -> {
- String message = formLambdaReason(lambdaType) + ", but " + v.message();
- state.reportMatch(buildDescription(tree).setMessage(message).build());
- });
- for (var entry : typesClosed.asMap().entrySet()) {
- var classSymbol = entry.getKey();
- var methods = entry.getValue();
- if (!hasImmutableAnnotation(classSymbol.type.tsym, state)) {
- String message =
- format(
- "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable.",
- formLambdaReason(lambdaType),
- methods.stream().map(Symbol::getSimpleName).collect(joining(", ")),
- classSymbol.getSimpleName());
- state.reportMatch(buildDescription(tree).setMessage(message).build());
- }
- }
+ checkClosedTypes(tree, state, lambdaType, analysis);
return NO_MATCH;
}
- private Violation checkClosedLambdaVariable(
- VarSymbol closedVariable,
- LambdaExpressionTree tree,
- ImmutableSet typarams,
- ImmutableAnalysis analysis) {
- if (!closedVariable.getKind().equals(ElementKind.FIELD)) {
- return analysis.isThreadSafeType(false, typarams, closedVariable.type);
- }
- return analysis.isFieldImmutable(
- Optional.empty(),
- typarams,
- (ClassSymbol) closedVariable.owner,
- (ClassType) closedVariable.owner.type,
- closedVariable,
- (t, v) -> buildDescription(tree));
- }
-
- private static String formLambdaReason(TypeSymbol typeSymbol) {
- return "This lambda implements @Immutable interface '" + typeSymbol.getSimpleName() + "'";
- }
-
private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) {
return immutableAnnotations.stream()
.anyMatch(annotation -> hasAnnotation(tsym, annotation, state));
}
- // check instantiations of `@ImmutableTypeParameter`s in method references
@Override
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
+ // check instantiations of `@ImmutableTypeParameter`s in method references
checkInvocation(tree, getSymbol(tree), ((JCMemberReference) tree).referentType, state);
+ ImmutableAnalysis analysis = createImmutableAnalysis(state);
+ TypeSymbol memberReferenceType = targetType(state).type().tsym;
+ Violation info =
+ analysis.checkInstantiation(
+ memberReferenceType.getTypeParameters(), getType(tree).getTypeArguments());
+
+ if (info.isPresent()) {
+ state.reportMatch(buildDescription(tree).setMessage(info.message()).build());
+ }
+ if (!hasImmutableAnnotation(memberReferenceType, state)) {
+ return NO_MATCH;
+ }
+ if (getSymbol(getReceiver(tree)) instanceof ClassSymbol) {
+ return NO_MATCH;
+ }
+ var receiverType = getReceiverType(tree);
+ ImmutableSet typarams =
+ immutableTypeParametersInScope(getSymbol(tree), state, analysis);
+ var violation =
+ analysis.isThreadSafeType(/* allowContainerTypeParameters= */ true, typarams, receiverType);
+ if (violation.isPresent()) {
+ return buildDescription(tree)
+ .setMessage(
+ "This method reference implements @Immutable interface "
+ + memberReferenceType.getSimpleName()
+ + ", but "
+ + violation.message())
+ .build();
+ }
return NO_MATCH;
}
@@ -380,6 +322,11 @@ public Description matchClass(ClassTree tree, VisitorState state) {
(Tree matched, Violation violation) ->
describeClass(matched, sym, annotation, violation));
+ Type superType = immutableSupertype(sym, state);
+ if (handleAnonymousClasses && superType != null && isLocal(sym)) {
+ checkClosedTypes(tree, state, superType.tsym, analysis);
+ }
+
if (!info.isPresent()) {
return NO_MATCH;
}
@@ -433,6 +380,10 @@ private Description handleAnonymousClass(
if (superType == null) {
return NO_MATCH;
}
+
+ if (handleAnonymousClasses) {
+ checkClosedTypes(tree, state, superType.tsym, analysis);
+ }
// We don't need to check that the superclass has an immutable instantiation.
// The anonymous instance can only be referred to using a superclass type, so
// the type arguments will be validated at any type use site where we care about
@@ -449,18 +400,142 @@ private Description handleAnonymousClass(
Optional.of(tree),
typarams,
ASTHelpers.getType(tree),
- new ViolationReporter() {
- @Override
- public Description.Builder describe(Tree tree, Violation info) {
- return describeAnonymous(tree, superType, info);
- }
- });
+ (t, i) -> describeAnonymous(t, superType, i));
if (!info.isPresent()) {
return NO_MATCH;
}
return describeAnonymous(tree, superType, info).build();
}
+ private void checkClosedTypes(
+ Tree lambdaOrAnonymousClass,
+ VisitorState state,
+ TypeSymbol lambdaType,
+ ImmutableAnalysis analysis) {
+ Set variablesClosed = new HashSet<>();
+ SetMultimap typesClosed = LinkedHashMultimap.create();
+ Set variablesOwnedByLambda = new HashSet<>();
+
+ new TreePathScanner() {
+ @Override
+ public Void visitVariable(VariableTree tree, Void unused) {
+ var symbol = getSymbol(tree);
+ variablesOwnedByLambda.add(symbol);
+ return super.visitVariable(tree, null);
+ }
+
+ @Override
+ public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
+ if (getReceiver(tree) == null) {
+ var symbol = getSymbol(tree);
+ if (!symbol.isStatic() && !symbol.isConstructor()) {
+ effectiveTypeOfThis(symbol, getCurrentPath(), state)
+ .filter(t -> !isSameType(t.type, getType(lambdaOrAnonymousClass), state))
+ .ifPresent(t -> typesClosed.put(t, symbol));
+ }
+ }
+ return super.visitMethodInvocation(tree, null);
+ }
+
+ @Override
+ public Void visitMemberSelect(MemberSelectTree tree, Void unused) {
+ // Note: member selects are not intrinsically problematic; the issue is what might be on the
+ // LHS of them, which is going to be handled by another visit* method.
+
+ // If we're only seeing a field access, don't complain about the fact we closed around
+ // `this`. This is special-case as it would otherwise be vexing to complain about accessing
+ // a field of type ImmutableList.
+ if (tree.getExpression() instanceof IdentifierTree
+ && getSymbol(tree) instanceof VarSymbol
+ && ((IdentifierTree) tree.getExpression()).getName().contentEquals("this")) {
+ handleIdentifier(getSymbol(tree));
+ return null;
+ }
+ return super.visitMemberSelect(tree, null);
+ }
+
+ @Override
+ public Void visitIdentifier(IdentifierTree tree, Void unused) {
+ handleIdentifier(getSymbol(tree));
+ return super.visitIdentifier(tree, null);
+ }
+
+ private void handleIdentifier(Symbol symbol) {
+ if (symbol instanceof VarSymbol
+ && !variablesOwnedByLambda.contains(symbol)
+ && !symbol.isStatic()) {
+ variablesClosed.add((VarSymbol) symbol);
+ }
+ }
+ }.scan(state.getPath(), null);
+
+ ImmutableSet typarams =
+ immutableTypeParametersInScope(getSymbol(lambdaOrAnonymousClass), state, analysis);
+ variablesClosed.stream()
+ .map(
+ closedVariable ->
+ checkClosedVariable(closedVariable, lambdaOrAnonymousClass, typarams, analysis))
+ .filter(Violation::isPresent)
+ .forEachOrdered(
+ v -> {
+ String message =
+ formAnonymousReason(lambdaOrAnonymousClass, lambdaType) + ", but " + v.message();
+ state.reportMatch(
+ buildDescription(lambdaOrAnonymousClass).setMessage(message).build());
+ });
+ for (var entry : typesClosed.asMap().entrySet()) {
+ var classSymbol = entry.getKey();
+ var methods = entry.getValue();
+ if (!hasImmutableAnnotation(classSymbol.type.tsym, state)) {
+ String message =
+ format(
+ "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable.",
+ formAnonymousReason(lambdaOrAnonymousClass, lambdaType),
+ methods.stream().map(Symbol::getSimpleName).collect(joining(", ")),
+ classSymbol.getSimpleName());
+ state.reportMatch(buildDescription(lambdaOrAnonymousClass).setMessage(message).build());
+ }
+ }
+ }
+
+ /**
+ * Gets the effective type of `this`, had the bare invocation of {@code symbol} been qualified
+ * with it.
+ */
+ private static Optional effectiveTypeOfThis(
+ MethodSymbol symbol, TreePath currentPath, VisitorState state) {
+ return stream(currentPath.iterator())
+ .filter(ClassTree.class::isInstance)
+ .map(t -> ASTHelpers.getSymbol((ClassTree) t))
+ .filter(c -> isSubtype(c.type, symbol.owner.type, state))
+ .findFirst();
+ }
+
+ private Violation checkClosedVariable(
+ VarSymbol closedVariable,
+ Tree tree,
+ ImmutableSet typarams,
+ ImmutableAnalysis analysis) {
+ if (!closedVariable.getKind().equals(ElementKind.FIELD)) {
+ return analysis.isThreadSafeType(false, typarams, closedVariable.type);
+ }
+ return analysis.isFieldImmutable(
+ Optional.empty(),
+ typarams,
+ (ClassSymbol) closedVariable.owner,
+ (ClassType) closedVariable.owner.type,
+ closedVariable,
+ (t, v) -> buildDescription(tree));
+ }
+
+ private static String formAnonymousReason(Tree tree, TypeSymbol typeSymbol) {
+ return "This "
+ + (tree instanceof LambdaExpressionTree ? "lambda" : "anonymous class")
+ + " implements @Immutable interface '"
+ + typeSymbol.getSimpleName()
+ + "'";
+ }
+
private Description.Builder describeAnonymous(Tree tree, Type superType, Violation info) {
String message =
format(
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodChecker.java
deleted file mode 100644
index 03a065f7aed..00000000000
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodChecker.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2014 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.threadsafety;
-
-import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.errorprone.BugPattern;
-import com.google.errorprone.VisitorState;
-import com.google.errorprone.annotations.concurrent.LockMethod;
-import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.MethodTree;
-import java.util.Set;
-
-/**
- * @author cushon@google.com (Liam Miller-Cushon)
- */
-@BugPattern(
- name = "LockMethodChecker",
- altNames = {"GuardedBy"},
- summary = "This method does not acquire the locks specified by its @LockMethod annotation",
- severity = ERROR)
-public class LockMethodChecker extends AbstractLockMethodChecker {
-
- @Override
- protected ImmutableList getLockExpressions(MethodTree tree) {
- LockMethod lockMethod = ASTHelpers.getAnnotation(tree, LockMethod.class);
- return lockMethod == null
- ? ImmutableList.of()
- : ImmutableList.copyOf(lockMethod.value());
- }
-
- @Override
- protected Set getActual(MethodTree tree, VisitorState state) {
- return ImmutableSet.copyOf(
- HeldLockAnalyzer.AcquiredLockFinder.find(tree.getBody(), state, GuardedByFlags.allOn()));
- }
-
- @Override
- protected Set getUnwanted(MethodTree tree, VisitorState state) {
- return ImmutableSet.copyOf(
- HeldLockAnalyzer.ReleasedLockFinder.find(tree.getBody(), state, GuardedByFlags.allOn()));
- }
-
- @Override
- protected String buildMessage(String unhandled) {
- return "The following locks are specified in this method's @LockMethod annotation but are not"
- + " acquired: "
- + unhandled;
- }
-}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodChecker.java
deleted file mode 100644
index da9c4475f25..00000000000
--- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodChecker.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2014 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.threadsafety;
-
-import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.errorprone.BugPattern;
-import com.google.errorprone.VisitorState;
-import com.google.errorprone.annotations.concurrent.UnlockMethod;
-import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.MethodTree;
-import java.util.Set;
-
-/**
- * @author cushon@google.com (Liam Miller-Cushon)
- */
-@BugPattern(
- name = "UnlockMethod",
- altNames = {"GuardedBy"},
- summary = "This method does not acquire the locks specified by its @UnlockMethod annotation",
- severity = ERROR)
-public class UnlockMethodChecker extends AbstractLockMethodChecker {
-
- @Override
- protected ImmutableList getLockExpressions(MethodTree tree) {
- UnlockMethod unlockMethod = ASTHelpers.getAnnotation(tree, UnlockMethod.class);
- return unlockMethod == null
- ? ImmutableList.of()
- : ImmutableList.copyOf(unlockMethod.value());
- }
-
- @Override
- protected Set getActual(MethodTree tree, VisitorState state) {
- return ImmutableSet.copyOf(
- HeldLockAnalyzer.ReleasedLockFinder.find(tree.getBody(), state, GuardedByFlags.allOn()));
- }
-
- @Override
- protected Set getUnwanted(MethodTree tree, VisitorState state) {
- return ImmutableSet.copyOf(
- HeldLockAnalyzer.AcquiredLockFinder.find(tree.getBody(), state, GuardedByFlags.allOn()));
- }
-
- @Override
- protected String buildMessage(String unhandled) {
- return "The following locks are specified by this method's @UnlockMethod anotation but are not"
- + " released: "
- + unhandled;
- }
-}
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/PreferJavaTimeOverload.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/PreferJavaTimeOverload.java
index 3aa0237793a..6ba1e62d586 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/time/PreferJavaTimeOverload.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/PreferJavaTimeOverload.java
@@ -33,7 +33,6 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
@@ -63,6 +62,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
import javax.annotation.Nullable;
/** This check suggests the use of {@code java.time}-based APIs, when available. */
@@ -394,7 +394,7 @@ private static boolean hasTimeSourceMethod(MethodInvocationTree tree, VisitorSta
private static boolean hasMatchingMethods(
Name name, Predicate predicate, Type startClass, Types types) {
Predicate matchesMethodPredicate =
- sym -> sym instanceof MethodSymbol && predicate.apply((MethodSymbol) sym);
+ sym -> sym instanceof MethodSymbol && predicate.test((MethodSymbol) sym);
// Iterate over all classes and interfaces that startClass inherits from.
for (Type superClass : types.closure(startClass)) {
diff --git a/core/src/main/java/com/google/errorprone/refaster/ExpressionTemplate.java b/core/src/main/java/com/google/errorprone/refaster/ExpressionTemplate.java
index 435bd44adb9..2bbc310edff 100644
--- a/core/src/main/java/com/google/errorprone/refaster/ExpressionTemplate.java
+++ b/core/src/main/java/com/google/errorprone/refaster/ExpressionTemplate.java
@@ -33,6 +33,7 @@
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreeScanner;
+import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.code.Types;
@@ -248,7 +249,8 @@ public Fix replace(ExpressionTemplateMatch match) {
*/
private static int getPrecedence(JCTree leaf, Context context) {
JCCompilationUnit comp = context.get(JCCompilationUnit.class);
- JCTree parent = TreeInfo.pathFor(leaf, comp).get(1);
+ JCTree parent =
+ (JCTree) JavacTrees.instance(context).getPath(comp, leaf).getParentPath().getLeaf();
// In general, this should match the logic in com.sun.tools.javac.tree.Pretty.
//
diff --git a/core/src/main/java/com/google/errorprone/refaster/Template.java b/core/src/main/java/com/google/errorprone/refaster/Template.java
index df1b5a16fd1..0fbbc435cf5 100644
--- a/core/src/main/java/com/google/errorprone/refaster/Template.java
+++ b/core/src/main/java/com/google/errorprone/refaster/Template.java
@@ -37,6 +37,7 @@
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ForAll;
import com.sun.tools.javac.code.Type.MethodType;
+import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.comp.Attr;
import com.sun.tools.javac.comp.AttrContext;
@@ -139,7 +140,14 @@ protected List expectedTypes(Inliner inliner) throws CouldNotResolveImport
Ordering.natural()
.immutableSortedCopy(
Iterables.filter(inliner.bindings.keySet(), PlaceholderExpressionKey.class))) {
- result.add(key.method.returnType().inline(inliner));
+ Type type = key.method.returnType().inline(inliner);
+ // Skip void placeholder expressions, because
+ // a) if the expected type is void, any actual type is acceptable
+ // b) these types are used as the argument types in a synthetic MethodType, and method
+ // argument types cannot be void
+ if (!type.getTag().equals(TypeTag.VOID)) {
+ result.add(type);
+ }
}
return List.from(result);
}
@@ -149,7 +157,7 @@ protected List expectedTypes(Inliner inliner) throws CouldNotResolveImport
* bound to the @BeforeTemplate method parameters, concatenated with the types of the expressions
* bound to expression placeholders, sorted by the name of the placeholder method.
*/
- protected List actualTypes(Inliner inliner) {
+ protected List actualTypes(Inliner inliner) throws CouldNotResolveImportException {
ArrayList result = new ArrayList<>();
ImmutableList argNames = expressionArgumentTypes().keySet().asList();
for (int i = 0; i < expressionArgumentTypes().size(); i++) {
@@ -177,7 +185,11 @@ protected List actualTypes(Inliner inliner) {
Ordering.natural()
.immutableSortedCopy(
Iterables.filter(inliner.bindings.keySet(), PlaceholderExpressionKey.class))) {
- result.add(inliner.getBinding(key).type);
+ Type keyType = key.method.returnType().inline(inliner);
+ // See comment in `expectedTypes` for why we skip void placeholder keys.
+ if (!keyType.getTag().equals(TypeTag.VOID)) {
+ result.add(inliner.getBinding(key).type);
+ }
}
return List.from(result);
}
diff --git a/core/src/main/java/com/google/errorprone/refaster/UBlank.java b/core/src/main/java/com/google/errorprone/refaster/UBlank.java
index f1cf3dd91f9..ac02a2c7efd 100644
--- a/core/src/main/java/com/google/errorprone/refaster/UBlank.java
+++ b/core/src/main/java/com/google/errorprone/refaster/UBlank.java
@@ -86,8 +86,7 @@ public Choice apply(UnifierWithUnconsumedStatem
int goodIndex = 0;
while (goodIndex < state.unconsumedStatements().size()) {
StatementTree stmt = state.unconsumedStatements().get(goodIndex);
- // If the statement refers to bound variables or doesn't always exit, stop consuming
- // statements.
+ // If the statement refers to bound variables or might exit, stop consuming statements.
if (firstNonNull(FORBIDDEN_REFERENCE_SCANNER.scan(stmt, state.unifier()), false)
|| ControlFlowVisitor.INSTANCE.visitStatement(stmt)
!= ControlFlowVisitor.Result.NEVER_EXITS) {
diff --git a/core/src/main/java/com/google/errorprone/refaster/UPlaceholderExpression.java b/core/src/main/java/com/google/errorprone/refaster/UPlaceholderExpression.java
index fc226f81f9c..6c579243221 100644
--- a/core/src/main/java/com/google/errorprone/refaster/UPlaceholderExpression.java
+++ b/core/src/main/java/com/google/errorprone/refaster/UPlaceholderExpression.java
@@ -147,7 +147,7 @@ public boolean reverify(Unifier unifier) {
@Override
protected Choice defaultAction(Tree node, Unifier unifier) {
// for now we only match JCExpressions
- if (placeholder().returnType().equals(UPrimitiveType.VOID) || !(node instanceof JCExpression)) {
+ if (!(node instanceof JCExpression)) {
return Choice.none();
}
JCExpression expr = (JCExpression) node;
diff --git a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java
index 8f41a09b039..829d5a3fdf9 100644
--- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java
+++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java
@@ -48,6 +48,7 @@
import com.google.errorprone.bugpatterns.BadImport;
import com.google.errorprone.bugpatterns.BadInstanceof;
import com.google.errorprone.bugpatterns.BadShiftAmount;
+import com.google.errorprone.bugpatterns.BanJNDI;
import com.google.errorprone.bugpatterns.BanSerializableRead;
import com.google.errorprone.bugpatterns.BareDotMetacharacter;
import com.google.errorprone.bugpatterns.BigDecimalEquals;
@@ -101,7 +102,6 @@
import com.google.errorprone.bugpatterns.DifferentNameButSame;
import com.google.errorprone.bugpatterns.DiscardedPostfixExpression;
import com.google.errorprone.bugpatterns.DistinctVarargsChecker;
-import com.google.errorprone.bugpatterns.DivZero;
import com.google.errorprone.bugpatterns.DoNotCallChecker;
import com.google.errorprone.bugpatterns.DoNotCallSuggester;
import com.google.errorprone.bugpatterns.DoNotClaimAnnotations;
@@ -121,6 +121,7 @@
import com.google.errorprone.bugpatterns.EqualsUnsafeCast;
import com.google.errorprone.bugpatterns.EqualsUsingHashCode;
import com.google.errorprone.bugpatterns.EqualsWrongThing;
+import com.google.errorprone.bugpatterns.ErroneousBitwiseExpression;
import com.google.errorprone.bugpatterns.ErroneousThreadPoolConstructorChecker;
import com.google.errorprone.bugpatterns.ExpectedExceptionChecker;
import com.google.errorprone.bugpatterns.ExtendingJUnitAssert;
@@ -168,6 +169,7 @@
import com.google.errorprone.bugpatterns.IntLongMath;
import com.google.errorprone.bugpatterns.InterfaceWithOnlyStatics;
import com.google.errorprone.bugpatterns.InterruptedExceptionSwallowed;
+import com.google.errorprone.bugpatterns.Interruption;
import com.google.errorprone.bugpatterns.InvalidPatternSyntax;
import com.google.errorprone.bugpatterns.InvalidTimeZoneID;
import com.google.errorprone.bugpatterns.InvalidZoneId;
@@ -247,6 +249,7 @@
import com.google.errorprone.bugpatterns.NullOptional;
import com.google.errorprone.bugpatterns.NullTernary;
import com.google.errorprone.bugpatterns.NullableConstructor;
+import com.google.errorprone.bugpatterns.NullableOnContainingClass;
import com.google.errorprone.bugpatterns.NullablePrimitive;
import com.google.errorprone.bugpatterns.NullablePrimitiveArray;
import com.google.errorprone.bugpatterns.NullableVoid;
@@ -282,7 +285,6 @@
import com.google.errorprone.bugpatterns.ProtoStringFieldReferenceEquality;
import com.google.errorprone.bugpatterns.ProtoTruthMixedDescriptors;
import com.google.errorprone.bugpatterns.ProtocolBufferOrdinal;
-import com.google.errorprone.bugpatterns.ProtosAsKeyOfSetOrMap;
import com.google.errorprone.bugpatterns.PublicApiNamedStreamShouldReturnStream;
import com.google.errorprone.bugpatterns.RandomCast;
import com.google.errorprone.bugpatterns.RandomModInteger;
@@ -500,11 +502,9 @@
import com.google.errorprone.bugpatterns.threadsafety.ImmutableChecker;
import com.google.errorprone.bugpatterns.threadsafety.ImmutableEnumChecker;
import com.google.errorprone.bugpatterns.threadsafety.ImmutableRefactoring;
-import com.google.errorprone.bugpatterns.threadsafety.LockMethodChecker;
import com.google.errorprone.bugpatterns.threadsafety.StaticGuardedByInstance;
import com.google.errorprone.bugpatterns.threadsafety.SynchronizeOnNonFinalField;
import com.google.errorprone.bugpatterns.threadsafety.ThreadPriorityCheck;
-import com.google.errorprone.bugpatterns.threadsafety.UnlockMethodChecker;
import com.google.errorprone.bugpatterns.time.DateChecker;
import com.google.errorprone.bugpatterns.time.DurationFrom;
import com.google.errorprone.bugpatterns.time.DurationGetTemporalUnit;
@@ -602,6 +602,7 @@ public static ScannerSupplier errorChecks() {
AutoValueConstructorOrderChecker.class,
BadAnnotationImplementation.class,
BadShiftAmount.class,
+ BanJNDI.class,
BoxedPrimitiveEquality.class,
BundleDeserializationCast.class,
ChainingConstructorIgnoresParameter.class,
@@ -629,6 +630,7 @@ public static ScannerSupplier errorChecks() {
DurationGetTemporalUnit.class,
DurationTemporalUnit.class,
DurationToLongTimeUnit.class,
+ EmptyTopLevelDeclaration.class,
EqualsHashCode.class,
EqualsNaN.class,
EqualsNull.class,
@@ -644,6 +646,7 @@ public static ScannerSupplier errorChecks() {
FromTemporalAccessor.class,
FunctionalInterfaceMethodChanged.class,
FuturesGetCheckedIllegalExceptionType.class,
+ FuzzyEqualsShouldNotBeUsedInEqualsMethod.class,
GetClassOnAnnotation.class,
GetClassOnClass.class,
GuardedByChecker.class,
@@ -701,6 +704,7 @@ public static ScannerSupplier errorChecks() {
NonFinalCompileTimeConstant.class,
NonRuntimeAnnotation.class,
NullTernary.class,
+ NullableOnContainingClass.class,
OptionalEquality.class,
OptionalMapUnusedValue.class,
OptionalOfRedundantMethod.class,
@@ -817,6 +821,7 @@ public static ScannerSupplier errorChecks() {
EqualsIncompatibleType.class,
EqualsUnsafeCast.class,
EqualsUsingHashCode.class,
+ ErroneousBitwiseExpression.class,
ErroneousThreadPoolConstructorChecker.class,
EscapedEntity.class,
ExtendingJUnitAssert.class,
@@ -1015,9 +1020,7 @@ public static ScannerSupplier errorChecks() {
DeduplicateConstants.class,
DepAnn.class,
DifferentNameButSame.class,
- DivZero.class,
EmptyIfStatement.class,
- EmptyTopLevelDeclaration.class,
EqualsBrokenForNull.class,
EqualsMissingNullable.class,
ExpectedExceptionChecker.class,
@@ -1035,7 +1038,6 @@ public static ScannerSupplier errorChecks() {
FloggerWithoutCause.class,
ForEachIterable.class,
FunctionalInterfaceClash.class,
- FuzzyEqualsShouldNotBeUsedInEqualsMethod.class,
HardCodedSdCardPath.class,
ImmutableMemberCollection.class,
ImmutableRefactoring.class,
@@ -1046,11 +1048,11 @@ public static ScannerSupplier errorChecks() {
InsecureCipherMode.class,
InterfaceWithOnlyStatics.class,
InterruptedExceptionSwallowed.class,
+ Interruption.class,
IterablePathParameter.class,
Java7ApiChecker.class,
Java8ApiChecker.class,
LambdaFunctionalInterface.class,
- LockMethodChecker.class,
LongLiteralLowerCaseSuffix.class,
MemberName.class,
MethodCanBeStatic.class,
@@ -1070,7 +1072,6 @@ public static ScannerSupplier errorChecks() {
PrimitiveArrayPassedToVarargsMethod.class,
PrivateConstructorForNoninstantiableModule.class,
PrivateConstructorForUtilityClass.class,
- ProtosAsKeyOfSetOrMap.class,
PublicApiNamedStreamShouldReturnStream.class,
QualifierWithTypeUse.class,
RedundantOverride.class,
@@ -1101,7 +1102,6 @@ public static ScannerSupplier errorChecks() {
TypeParameterNaming.class,
TypeToString.class,
UngroupedOverloads.class,
- UnlockMethodChecker.class,
UnnecessarilyFullyQualified.class,
UnnecessarilyVisible.class,
UnnecessaryAnonymousClass.class,
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java
similarity index 57%
rename from core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java
rename to core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java
index a6482e5f232..e89b1bd4e93 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013 The Error Prone Authors.
+ * Copyright 2020 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.
@@ -16,27 +16,37 @@
package com.google.errorprone.bugpatterns;
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-/**
- * @author cushon@google.com (Liam Miller-Cushon)
- */
+/** {@link BanJNDI}Test */
@RunWith(JUnit4.class)
-public class DivZeroTest {
-
+public class BanJNDITest {
private final CompilationTestHelper compilationHelper =
- CompilationTestHelper.newInstance(DivZero.class, getClass());
+ CompilationTestHelper.newInstance(BanJNDI.class, getClass());
+
+ private final BugCheckerRefactoringTestHelper refactoringHelper =
+ BugCheckerRefactoringTestHelper.newInstance(BanJNDI.class, getClass());
@Test
public void testPositiveCase() {
- compilationHelper.addSourceFile("DivZeroPositiveCases.java").doTest();
+ compilationHelper.addSourceFile("BanJNDIPositiveCases.java").doTest();
}
@Test
public void testNegativeCase() {
- compilationHelper.addSourceFile("DivZeroNegativeCases.java").doTest();
+ compilationHelper.addSourceFile("BanJNDINegativeCases.java").doTest();
+ }
+
+ @Test
+ public void testNegativeCaseUnchanged() {
+ refactoringHelper
+ .addInput("BanJNDINegativeCases.java")
+ .expectUnchanged()
+ .setArgs("-XepCompilingTestOnlyCode")
+ .doTest();
}
}
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java
index 65f5f6cf2b6..298fa258553 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java
@@ -16,11 +16,20 @@
package com.google.errorprone.bugpatterns;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import com.google.auto.value.processor.AutoBuilderProcessor;
import com.google.auto.value.processor.AutoValueProcessor;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.CompilationTestHelper;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -33,6 +42,8 @@ public class CheckReturnValueTest {
private final CompilationTestHelper compilationHelper =
CompilationTestHelper.newInstance(CheckReturnValue.class, getClass());
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
@Test
public void testPositiveCases() {
compilationHelper.addSourceFile("CheckReturnValuePositiveCases.java").doTest();
@@ -846,6 +857,38 @@ public void constructor_withoutCrvAnnotation() {
.doTest();
}
+ @Test
+ public void allMethods_withoutCIRVAnnotation() {
+ compilationHelperLookingAtAllMethods()
+ .addSourceLines(
+ "Test.java",
+ "class Test {",
+ " public int bar() { return 42; }",
+ " public static void foo() {",
+ " // BUG: Diagnostic contains: Ignored return value of 'bar', which wasn't"
+ + " annotated with @CanIgnoreReturnValue",
+ " new Test().bar();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void allMethods_withExternallyConfiguredIgnoreList() {
+ compileWithExternalApis("java.util.List#add(java.lang.Object)")
+ .addSourceLines(
+ "Test.java",
+ "import java.util.List;",
+ "class Test {",
+ " public static void foo(List x) {",
+ " x.add(42);",
+ " // BUG: Diagnostic contains: Ignored return value of 'get'",
+ " x.get(0);",
+ " }",
+ "}")
+ .doTest();
+ }
+
@Test
public void usingElementInTestExpected() {
compilationHelperLookingAtAllConstructors()
@@ -1003,4 +1046,21 @@ private CompilationTestHelper compilationHelperLookingAtAllConstructors() {
return compilationHelper.setArgs(
"-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true");
}
+
+ private CompilationTestHelper compilationHelperLookingAtAllMethods() {
+ return compilationHelper.setArgs("-XepOpt:" + CheckReturnValue.CHECK_ALL_METHODS + "=true");
+ }
+
+ private CompilationTestHelper compileWithExternalApis(String... apis) {
+ try {
+ Path file = temporaryFolder.newFile().toPath();
+ Files.writeString(file, Joiner.on('\n').join(apis), UTF_8);
+
+ return compilationHelper.setArgs(
+ "-XepOpt:" + CheckReturnValue.CHECK_ALL_METHODS + "=true",
+ "-XepOpt:CheckReturnValue:ApiExclusionList=" + file);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
}
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpressionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpressionTest.java
new file mode 100644
index 00000000000..e1477d5e286
--- /dev/null
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpressionTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ErroneousBitwiseExpression}. */
+@RunWith(JUnit4.class)
+public final class ErroneousBitwiseExpressionTest {
+ private final CompilationTestHelper helper =
+ CompilationTestHelper.newInstance(ErroneousBitwiseExpression.class, getClass());
+
+ @Test
+ public void bitwiseAnd() {
+ helper
+ .addSourceLines(
+ "Test.java", //
+ "class Test {",
+ " // BUG: Diagnostic contains: 1 | 2",
+ " double flags = 1 & 2;",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bitwiseAnd_noFinding() {
+ helper
+ .addSourceLines(
+ "Test.java", //
+ "class Test {",
+ " double flags = 2 & 10;",
+ "}")
+ .doTest();
+ }
+}
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/FieldCanBeFinalTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/FieldCanBeFinalTest.java
index 9d7a977bb07..4a5f36aacea 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/FieldCanBeFinalTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/FieldCanBeFinalTest.java
@@ -73,6 +73,21 @@ public void keepAnnotatedFields_ignored() {
.doTest();
}
+ @Test
+ public void injectAnnotatedFields_ignored() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java",
+ "import javax.inject.Inject;",
+ "class Test {",
+ " @Inject private int x;",
+ " Test() {",
+ " x = 42;",
+ " }",
+ "}")
+ .doTest();
+ }
+
@Test
public void initializerBlocks() {
compilationHelper
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java
new file mode 100644
index 00000000000..2c7e66c192e
--- /dev/null
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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;
+
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link Interruption}Test */
+@RunWith(JUnit4.class)
+public class InterruptionTest {
+
+ private final CompilationTestHelper compilationHelper =
+ CompilationTestHelper.newInstance(Interruption.class, getClass());
+
+ @Test
+ public void positive() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java",
+ "import java.util.concurrent.Future;",
+ "class Test {",
+ " void f(Future> f, boolean b) {",
+ " // BUG: Diagnostic contains: f.cancel(false)",
+ " f.cancel(true);",
+ " // BUG: Diagnostic contains: f.cancel(false)",
+ " f.cancel(b);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void positiveClosingFuture() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java",
+ "import com.google.common.util.concurrent.ClosingFuture;",
+ "class Test {",
+ " void f(ClosingFuture> f) {",
+ " // BUG: Diagnostic contains: f.cancel(false)",
+ " f.cancel(true);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void positiveInterrupt() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java", //
+ "class Test {",
+ " void f(Thread t) {",
+ " // BUG: Diagnostic contains:",
+ " t.interrupt();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void negative() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java",
+ "import java.util.concurrent.Future;",
+ "class Test {",
+ " void f(Future> f) {",
+ " f.cancel(false);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void negativeWasInterrupted() {
+ compilationHelper
+ .addSourceLines(
+ "Test.java",
+ "import com.google.common.util.concurrent.AbstractFuture;",
+ "class Test extends AbstractFuture