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.errorprone error_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.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 error-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.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 error-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.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 error-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 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 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 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 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 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 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 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 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 {", + " void f() {", + " cancel(wasInterrupted());", + " }", + "}") + .doTest(); + } + + @Test + public void negativeDelegate() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.util.concurrent.AbstractFuture;", + "import java.util.concurrent.Future;", + "class Test extends AbstractFuture {", + " void f(Future f) {", + " new AbstractFuture() {", + " @Override", + " public boolean cancel(boolean mayInterruptIfRunning) {", + " return f.cancel(mayInterruptIfRunning);", + " }", + " };", + " }", + "}") + .doTest(); + } + + @Test + public void negativeInterrupt() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " void f(Thread t) {", + " Thread.currentThread().interrupt();", + " }", + "}") + .doTest(); + } + + @Test + public void negativeInTestonlyCode() { + compilationHelper + .addSourceLines( + "Test.java", + "import org.junit.Test;", + "import org.junit.runner.RunWith;", + "import org.junit.runners.JUnit4;", + "import java.util.concurrent.Future;", + "@RunWith(JUnit4.class)", + "class FutureTest {", + " Future f;", + " @Test", + " public void t() {", + " f.cancel(true);", + " }", + "}") + .setArgs("-XepCompilingTestOnlyCode") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/JdkObsoleteTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/JdkObsoleteTest.java index 8dc9e9c047e..62c343372b8 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/JdkObsoleteTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/JdkObsoleteTest.java @@ -161,6 +161,28 @@ public void stringBufferRefactoringTest() { .doTest(); } + @Test + public void stringBufferRefactoringTest_usingVar() { + BugCheckerRefactoringTestHelper.newInstance(JdkObsolete.class, getClass()) + .addInputLines( + "in/Test.java", // + "class Test {", + " String f() {", + " var sb = new StringBuffer();", + " return sb.append(42).toString();", + " }", + "}") + .addOutputLines( + "out/Test.java", // + "class Test {", + " String f() {", + " var sb = new StringBuilder();", + " return sb.append(42).toString();", + " }", + "}") + .doTest(); + } + /** A test input. */ public interface Lib { Enumeration foos(); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MixedArrayDimensionsTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MixedArrayDimensionsTest.java index f628c10a3fa..c7ded2d64f5 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MixedArrayDimensionsTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MixedArrayDimensionsTest.java @@ -94,7 +94,9 @@ public void negativeInSimpleCharStream() { .addSourceLines( "SimpleCharStream.java", // "final class SimpleCharStream {", - " int a[];", + " void test() {", + " int a[];", + " }", "}") .doTest(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ModifiedButNotUsedTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ModifiedButNotUsedTest.java index f5d0bc12de3..7008028c53a 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ModifiedButNotUsedTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ModifiedButNotUsedTest.java @@ -218,6 +218,9 @@ public void proto() { " TestProtoMessage.Builder proto3 =", " // BUG: Diagnostic contains:", " TestProtoMessage.getDefaultInstance().toBuilder().clearMessage();", + " TestProtoMessage.Builder proto4 =", + " // BUG: Diagnostic contains:", + " TestProtoMessage.getDefaultInstance().toBuilder().clear();", " }", "}") .doTest(); @@ -307,7 +310,7 @@ public void protoUnusedExpression() { } @Test - public void protoUnusedButNotModified() { + public void protoBuilderMergeFrom() { compilationHelper .addSourceLines( "Test.java", @@ -315,7 +318,7 @@ public void protoUnusedButNotModified() { "import com.google.errorprone.bugpatterns.proto.ProtoTest.TestProtoMessage;", "class Test {", " void foo(TestProtoMessage proto) throws Exception {", - // Consider mergeFrom as a use, given it throws a checked exception. + " // BUG: Diagnostic contains:", " TestProtoMessage.newBuilder().mergeFrom(new byte[0]).build();", " }", "}") diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MultipleTopLevelClassesTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MultipleTopLevelClassesTest.java index 047c52a4e77..3a5591eea26 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MultipleTopLevelClassesTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MultipleTopLevelClassesTest.java @@ -33,9 +33,10 @@ public void twoClasses() { compilationHelper .addSourceLines( "a/A.java", - "// BUG: Diagnostic contains: one top-level class declaration, instead found: One, Two", "package a;", + "// BUG: Diagnostic contains: one top-level class declaration, instead found: One, Two", "class One {}", + "// BUG: Diagnostic contains:", "class Two {}") .doTest(); } @@ -57,6 +58,7 @@ public void defaultPackage() { "a/A.java", // "// BUG: Diagnostic contains:", "class A {}", + "// BUG: Diagnostic contains:", "class B {}") .doTest(); } @@ -74,7 +76,12 @@ public void suppression() { @Test public void emptyDeclaration() { - compilationHelper.addSourceLines("a/A.java", "package a;", "class Test {};").doTest(); + compilationHelper + .addSourceLines( + "a/A.java", // + "package a;", + "class Test {};") + .doTest(); } @Test @@ -82,14 +89,17 @@ public void semiInImportList() { compilationHelper .addSourceLines( "a/A.java", + "package a;", "// BUG: Diagnostic contains:", "// one top-level class declaration, instead found: Test, Extra", - "package a;", "import java.util.List;;", + "// BUG: Diagnostic contains:", "import java.util.ArrayList;", + "// BUG: Diagnostic contains:", "class Test {", " List xs = new ArrayList<>();", "}", + "// BUG: Diagnostic contains:", "class Extra {}") .doTest(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/NonCanonicalTypeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/NonCanonicalTypeTest.java index a79822ae073..7f5cdaba6bc 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/NonCanonicalTypeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/NonCanonicalTypeTest.java @@ -127,6 +127,21 @@ public void negative() { .doTest(); } + @Test + public void qualifiedName_inLambdaParameter_cantFix() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.function.Function;", + "class Test {", + " interface Rec extends Function {}\n", + " void run() {", + " Rec f = x -> x.apply(x);", + " }", + "}") + .doTest(); + } + @Test public void qualifiedName_ambiguous() { compilationHelper diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/NullableOnContainingClassTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/NullableOnContainingClassTest.java new file mode 100644 index 00000000000..e5c6c10605a --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/NullableOnContainingClassTest.java @@ -0,0 +1,99 @@ +/* + * 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 NullableOnContainingClass}. */ +@RunWith(JUnit4.class) +public final class NullableOnContainingClassTest { + public final CompilationTestHelper helper = + CompilationTestHelper.newInstance(NullableOnContainingClass.class, getClass()); + + @Test + public void annotationNotNamedNullable_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static java.lang.annotation.ElementType.TYPE_USE;", + "import java.lang.annotation.Target;", + "class A {", + " @Target(TYPE_USE)", + " @interface Anno {}", + " class B {}", + " void test(@Anno A.B x) {}", + " void test2(A.@Anno B x) {}", + "}") + .doTest(); + } + + @Test + public void annotationNamedNullable_annotatingOuterClass() { + helper + .addSourceLines( + "Test.java", + "import static java.lang.annotation.ElementType.TYPE_USE;", + "import java.lang.annotation.Target;", + "import java.util.List;", + "class A {", + " @Target(TYPE_USE)", + " @interface Nullable {}", + " class B {}", + " // BUG: Diagnostic contains: A.@Nullable B", + " void test(@Nullable A.B x) {}", + " // BUG: Diagnostic contains: List< A.@Nullable B>", + " void test2(List<@Nullable A.B> x) {}", + "}") + .doTest(); + } + + @Test + public void annotationNamedNullable_annotatingInImplements() { + helper + .addSourceLines( + "Test.java", + "import static java.lang.annotation.ElementType.TYPE_USE;", + "import java.lang.annotation.Target;", + "import java.util.List;", + "interface A {", + " @Target(TYPE_USE)", + " @interface Nullable {}", + " // BUG: Diagnostic contains: A.@Nullable B", + " abstract class B implements List<@Nullable A.B> {}", + "}") + .doTest(); + } + + @Test + public void annotationNamedNullable_annotatingInnerClass() { + helper + .addSourceLines( + "Test.java", + "import static java.lang.annotation.ElementType.TYPE_USE;", + "import java.lang.annotation.Target;", + "class A {", + " @Target(TYPE_USE)", + " @interface Nullable {}", + " class B {}", + " void test(A.@Nullable B x) {}", + "}") + .doTest(); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMapTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMapTest.java deleted file mode 100644 index 53702a5dbb0..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMapTest.java +++ /dev/null @@ -1,167 +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 com.google.errorprone.CompilationTestHelper; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * {@link ProtosAsKeyOfSetOrMap}Test - * - * @author seibelsabrina@google.com (Sabrina Seibel) - */ -@RunWith(JUnit4.class) -public final class ProtosAsKeyOfSetOrMapTest { - - private final CompilationTestHelper compilationHelper = - CompilationTestHelper.newInstance(ProtosAsKeyOfSetOrMap.class, getClass()); - - @Ignore("b/74365407 test proto sources are broken") - @Test - public void positive() { - compilationHelper - .addSourceLines( - "Test.java", - "import java.util.Arrays;", - "import java.util.Set;", - "import java.util.Map;", - "import java.util.LinkedHashMap;", - "import java.util.HashMap;", - "import java.util.HashSet;", - "import java.util.Collection;", - "import java.util.concurrent.ConcurrentHashMap;", - "import com.google.common.collect.Sets;", - "import com.google.common.collect.Maps;", - "import com.google.common.collect.HashMultiset;", - "import com.google.common.collect.LinkedHashMultiset;", - "import com.google.common.collect.HashBiMap;", - "import com.google.common.collect.HashMultimap;", - "import com.google.common.collect.LinkedHashMultimap;", - "import com.google.common.collect.ArrayListMultimap;", - "import com.google.common.collect.LinkedListMultimap;", - "import com.google.errorprone.bugpatterns.proto.ProtoTest.TestProtoMessage;", - "import com.google.protobuf.InvalidProtocolBufferException;", - "import com.google.protobuf.ByteString;", - "class Test {", - " void f(Collection x, TestProtoMessage m)" - + " throws InvalidProtocolBufferException {", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " Map testNewMap = Maps.newHashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " Set testNewSet = Sets.newHashSet();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashMap testNewHashMap = Maps.newHashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashSet testNewHashSet = Sets.newHashSet();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " Map testMap = new HashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " Set testSet = new HashSet();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashMap testHashMap = new HashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashSet testHashSet = new HashSet();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashMultimap testHashMultimap =" - + "HashMultimap.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " ArrayListMultimap testArrayListMultimap" - + " = ArrayListMultimap.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " LinkedHashMultimap testLinkedHashMultimap" - + "= LinkedHashMultimap.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " LinkedListMultimap testLinkedListMultimap" - + "= LinkedListMultimap.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashBiMap testHashBiMap = HashBiMap.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " LinkedHashMap testLinkedHashMap" - + "= new LinkedHashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " ConcurrentHashMap testConcurrentHashMap" - + "= new ConcurrentHashMap();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " LinkedHashMultiset testLinkedHashMultiSet" - + "= LinkedHashMultiset.create();", - " // BUG: Diagnostic contains: ProtosAsKeyOfSetOrMap", - " HashMultiset testHashMultiSet = HashMultiset.create();", - " }", - "}") - .doTest(); - } - - @Test - public void negative() { - compilationHelper - .addSourceLines( - "Test.java", - "import java.util.Arrays;", - "import java.util.Set;", - "import java.util.Map;", - "import java.util.LinkedHashMap;", - "import java.util.concurrent.ConcurrentHashMap;", - "import com.google.common.collect.Sets;", - "import com.google.common.collect.Maps;", - "import java.util.HashMap;", - "import java.util.HashSet;", - "import java.util.TreeSet;", - "import com.google.common.collect.HashMultiset;", - "import com.google.common.collect.LinkedHashMultiset;", - "import com.google.common.collect.HashBiMap;", - "import com.google.common.collect.HashMultimap;", - "import com.google.common.collect.LinkedHashMultimap;", - "import com.google.common.collect.ArrayListMultimap;", - "import com.google.common.collect.LinkedListMultimap;", - "import com.google.common.collect.Ordering;", - "class Test {", - " public static void main(String[] args) {", - " Map testMap = new HashMap();", - " Set testSet = new HashSet();", - " HashMap testHashMap = new HashMap();", - " HashSet testHashSet = new HashSet();", - " Set testSet2 = new HashSet();", - " Map testMap2 = new HashMap();", - " Map mapFromMethod = Maps.newHashMap();", - " Set setFromMethod = Sets.newHashSet();", - " Set thisShouldWork = new TreeSet" - + "(Ordering.natural().lexicographical().onResultOf(Arrays::asList));", - " HashMultimap testHashMultimap = HashMultimap.create();", - " ArrayListMultimap testArrayListMultimap" - + " = ArrayListMultimap.create();", - " LinkedHashMultimap testLinkedHashMultimap" - + "= LinkedHashMultimap.create();", - " LinkedListMultimap testLinkedListMultimap" - + "= LinkedListMultimap.create();", - " HashBiMap testHashBiMap = HashBiMap.create();", - " LinkedHashMap testLinkedHashMap" - + "= new LinkedHashMap();", - " ConcurrentHashMap testConcurrentHashMap" - + "= new ConcurrentHashMap();", - " HashMultiset testHashMultiSet = HashMultiset.create();", - " LinkedHashMultiset testLinkedHashMultiSet" - + "= LinkedHashMultiset.create();", - " }", - "}") - .doTest(); - } -} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java index ad7253ecfe7..fd9cb26cc69 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryLambdaTest.java @@ -316,4 +316,17 @@ public void variable_notAFunctionalInterface() { .expectUnchanged() .doTest(); } + + @Test + public void recursiveLambda_ignored() { + testHelper + .addInputLines( + "Test.java", + "import java.util.function.Predicate;", + "class Test {", + " private static final Predicate F = x -> Test.F.test(x);", + "}") + .expectUnchanged() + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java index ebfa5d86699..ce97355f5ff 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java @@ -32,7 +32,11 @@ public class UnsynchronizedOverridesSynchronizedTest { public void positive() { compilationHelper .addSourceLines( - "test/Super.java", "package test;", "class Super {", " synchronized void f() {}", "}") + "test/Super.java", // + "package test;", + "class Super {", + " synchronized void f() {}", + "}") .addSourceLines( "test/Test.java", "package test;", @@ -153,6 +157,47 @@ public void ignoreEmptyOverride() { " super.f();", " }", " }", + " class D extends Lib {", + " public void f() {", + " return;", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void ignoreOverrideThatReturnsThis() { + compilationHelper + .addSourceLines( + "test/Test.java", + "package test;", + "abstract class Test extends Throwable {", + " @Override", + " public Throwable fillInStackTrace() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void ignoreOverrideThatReturnsConstant() { + compilationHelper + .addSourceLines( + "A.java", // + "class A {", + " synchronized int f() {", + " return -1;", + " }", + "}") + .addSourceLines( + "B.java", + "class B extends A {", + " @Override", + " public int f() {", + " return 42;", + " }", "}") .doTest(); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java new file mode 100644 index 00000000000..3e99528ec75 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java @@ -0,0 +1,147 @@ +/* + * 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.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link Api}. */ +@RunWith(JUnit4.class) +public final class ApiTest { + + private static final ImmutableSet UNPARSEABLE_APIS = + ImmutableSet.of( + "", + "#()", + "#(java.lang.String)", + "#foo()", + "#foo(java.lang.String)", + "java.lang.String", + "java.lang.String##foo()", + "java.lang.String#fo#o()", + "java.lang.String#()", + "java.lang.String#foo)()", + "java.lang.String#foo)(", + "java.lang.String#(,)", + "java.lang.String#get(int[][)", + "java.lang.String#get(int[[])", + "java.lang.String#get(int[]])", + "java.lang.String#get(int])", + "java.lang.String#get(int[)", + "java.lang.String#get(int[]a)", + "java.lang.String#<>()", + "java.lang.String#hi<>()", + "java.lang.String#<>hi()", + "java.lang.String#hi()", + "java.lang.String#hi()", + "java.lang.String#()", + "java.lang.String#((java.lang.String)", + "java.lang.String#(java.lang.String", + "java.lang.String#(java.lang.String,)", + "java.lang.String#(,java.lang.String)", + "java.lang.String#(java.lang.String))"); + + @Test + public void parseApi_badInputs() { + // TODO(b/223670489): would be nice to use expectThrows() here + for (String badApi : UNPARSEABLE_APIS) { + assertThrows( + "Api.parse(\"" + badApi + "\")", IllegalArgumentException.class, () -> Api.parse(badApi)); + } + } + + @Test + public void parseApi_constructorWithoutParams() { + String string = "com.google.async.promisegraph.testing.TestPromiseGraphModule#()"; + Api api = Api.parse(string); + assertThat(api.className()) + .isEqualTo("com.google.async.promisegraph.testing.TestPromiseGraphModule"); + assertThat(api.methodName()).isEqualTo(""); + assertThat(api.parameterTypes()).isEmpty(); + assertThat(api.isConstructor()).isTrue(); + assertThat(api.toString()).isEqualTo(string); + } + + @Test + public void parseApi_constructorWithParams() { + String string = "com.google.api.client.http.GenericUrl#(java.lang.String)"; + Api api = Api.parse(string); + assertThat(api.className()).isEqualTo("com.google.api.client.http.GenericUrl"); + assertThat(api.methodName()).isEqualTo(""); + assertThat(api.parameterTypes()).containsExactly("java.lang.String").inOrder(); + assertThat(api.isConstructor()).isTrue(); + assertThat(api.toString()).isEqualTo(string); + } + + @Test + public void parseApi_methodWithoutParams() { + String string = "com.google.api.services.drive.model.File#getId()"; + Api api = Api.parse(string); + assertThat(api.className()).isEqualTo("com.google.api.services.drive.model.File"); + assertThat(api.methodName()).isEqualTo("getId"); + assertThat(api.parameterTypes()).isEmpty(); + assertThat(api.isConstructor()).isFalse(); + assertThat(api.toString()).isEqualTo(string); + } + + @Test + public void parseApi_methodWithParamsAndSpaces() { + String string = + " com.google.android.libraries.stitch.binder.Binder" + + "#get(android.content.Context , java.lang.Class) "; + Api api = Api.parse(string); + assertThat(api.methodName()).isEqualTo("get"); + assertThat(api.parameterTypes()) + .containsExactly("android.content.Context", "java.lang.Class") + .inOrder(); + assertThat(api.isConstructor()).isFalse(); + assertThat(api.toString()).isEqualTo(whitespace().removeFrom(string)); + } + + @Test + public void parseApi_methodWithArray() { + String string = + "com.google.inject.util.Modules.OverriddenModuleBuilder#with(com.google.inject.Module[],int[][][])"; + Api api = Api.parse(string); + assertThat(api.className()).isEqualTo("com.google.inject.util.Modules.OverriddenModuleBuilder"); + assertThat(api.methodName()).isEqualTo("with"); + assertThat(api.parameterTypes()) + .containsExactly("com.google.inject.Module[]", "int[][][]") + .inOrder(); + assertThat(api.isConstructor()).isFalse(); + assertThat(api.toString()).isEqualTo(string); + } + + @Test + public void parseApi_methodWithVarargs_b231250004() { + String string = "com.beust.jcommander.JCommander#(java.lang.Object,java.lang.String...)"; + Api api = Api.parse(string); + assertThat(api.className()).isEqualTo("com.beust.jcommander.JCommander"); + assertThat(api.methodName()).isEqualTo(""); + assertThat(api.parameterTypes()) + .containsExactly("java.lang.Object", "java.lang.String...") + .inOrder(); + assertThat(api.isConstructor()).isTrue(); + assertThat(api.toString()).isEqualTo(string); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java index 76385ab580a..2296ee81f86 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java @@ -27,14 +27,18 @@ /** {@link EqualsMissingNullable}Test */ @RunWith(JUnit4.class) public class EqualsMissingNullableTest { - private final CompilationTestHelper helper = + private final CompilationTestHelper conservativeHelper = CompilationTestHelper.newInstance(EqualsMissingNullable.class, getClass()); - private final BugCheckerRefactoringTestHelper refactoringHelper = - BugCheckerRefactoringTestHelper.newInstance(EqualsMissingNullable.class, getClass()); + private final CompilationTestHelper aggressiveHelper = + CompilationTestHelper.newInstance(EqualsMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + private final BugCheckerRefactoringTestHelper aggressiveRefactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(EqualsMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); @Test public void testPositive() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "abstract class Foo {", @@ -46,7 +50,7 @@ public void testPositive() { @Test public void testDeclarationAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import javax.annotation.Nullable;", @@ -64,7 +68,7 @@ public void testDeclarationAnnotatedLocation() { @Test public void testTypeAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import org.checkerframework.checker.nullness.qual.Nullable;", @@ -82,7 +86,7 @@ public void testTypeAnnotatedLocation() { @Test public void testNegativeAlreadyAnnotated() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "import javax.annotation.Nullable;", @@ -92,9 +96,22 @@ public void testNegativeAlreadyAnnotated() { .doTest(); } + @Test + public void testNegativeAlreadyAnnotatedWithProtobufAnnotation() { + aggressiveHelper + .addSourceLines( + "ProtoMethodAcceptsNullParameter.java", "@interface ProtoMethodAcceptsNullParameter {}") + .addSourceLines( + "Foo.java", + "abstract class Foo {", + " public abstract boolean equals(@ProtoMethodAcceptsNullParameter Object o);", + "}") + .doTest(); + } + @Test public void testNegativeNotObjectEquals() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "abstract class Foo {", @@ -102,4 +119,29 @@ public void testNegativeNotObjectEquals() { "}") .doTest(); } + + @Test + public void testPositiveConservativeNullMarked() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "abstract class Foo {", + " // BUG: Diagnostic contains: @Nullable", + " public abstract boolean equals(Object o);", + "}") + .doTest(); + } + + @Test + public void testNegativeConservativeNotNullMarked() { + conservativeHelper + .addSourceLines( + "Foo.java", // + "abstract class Foo {", + " public abstract boolean equals(Object o);", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullableTest.java index 1cca7bfff13..a211ffcb3c8 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ParameterMissingNullableTest.java @@ -27,14 +27,18 @@ /** {@link ParameterMissingNullable}Test */ @RunWith(JUnit4.class) public class ParameterMissingNullableTest { - private final CompilationTestHelper helper = + private final CompilationTestHelper conservativeHelper = CompilationTestHelper.newInstance(ParameterMissingNullable.class, getClass()); - private final BugCheckerRefactoringTestHelper refactoringHelper = - BugCheckerRefactoringTestHelper.newInstance(ParameterMissingNullable.class, getClass()); + private final CompilationTestHelper aggressiveHelper = + CompilationTestHelper.newInstance(ParameterMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + private final BugCheckerRefactoringTestHelper aggressiveRefactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(ParameterMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); @Test public void testPositiveIf() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -50,7 +54,7 @@ public void testPositiveIf() { @Test public void testPositiveIfWithUnrelatedThrow() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -71,7 +75,7 @@ public void testPositiveIfWithUnrelatedThrow() { public void testPositiveDespiteWhileLoop() { // TODO(cpovirk): This doesn't look "positive" to me. // TODO(cpovirk): Also, I *think* the lack of braces on the while() loop is intentional? - helper + aggressiveHelper .addSourceLines( "Foo.java", "import static com.google.common.base.Preconditions.checkArgument;", @@ -86,7 +90,7 @@ public void testPositiveDespiteWhileLoop() { @Test public void testPositiveTernary() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -99,9 +103,73 @@ public void testPositiveTernary() { .doTest(); } + @Test + public void testPositiveCallToMethod() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " void foo(Integer i) {}", + " void bar() {", + " // BUG: Diagnostic contains: @Nullable", + " foo(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveCallToTopLevelConstructor() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " Foo(Integer i) {}", + " void bar() {", + " // BUG: Diagnostic contains: @Nullable", + " new Foo(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveCallToNestedConstructor() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " static class Nested {", + " Nested(Integer i) {}", + " }", + " void bar() {", + " // BUG: Diagnostic contains: @Nullable", + " new Foo.Nested(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeCallToNestedConstructor() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " class Nested {", + " Nested(Integer i) {}", + " }", + " void bar() {", + // TODO(cpovirk): Recognize this. + " new Nested(null);", + " }", + "}") + .doTest(); + } + @Test public void testDeclarationAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import javax.annotation.Nullable;", @@ -127,7 +195,7 @@ public void testDeclarationAnnotatedLocation() { @Test public void testTypeAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import org.checkerframework.checker.nullness.qual.Nullable;", @@ -153,7 +221,7 @@ public void testTypeAnnotatedLocation() { @Test public void testNegativeAlreadyAnnotated() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "import javax.annotation.Nullable;", @@ -169,7 +237,7 @@ public void testNegativeAlreadyAnnotated() { @Test public void testNegativeCasesAlreadyTypeAnnotatedInnerClass() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "import org.checkerframework.checker.nullness.qual.Nullable;", @@ -187,7 +255,7 @@ public void testNegativeCasesAlreadyTypeAnnotatedInnerClass() { @Test public void testNegativePreconditionCheckMethod() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "import static com.google.common.base.Preconditions.checkArgument;", @@ -201,7 +269,7 @@ public void testNegativePreconditionCheckMethod() { @Test public void testNegativeOtherCheckMethod() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -215,7 +283,7 @@ public void testNegativeOtherCheckMethod() { @Test public void testNegativeAssert() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -228,7 +296,7 @@ public void testNegativeAssert() { @Test public void testNegativeCheckNotAgainstNull() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -243,7 +311,7 @@ public void testNegativeCheckNotAgainstNull() { @Test public void testNegativeCheckOfNonParameter() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -259,7 +327,7 @@ public void testNegativeCheckOfNonParameter() { @Test public void testNegativeThrow() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -277,7 +345,7 @@ public void testNegativeThrow() { @Test public void testNegativeCreateException() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -295,7 +363,7 @@ public void testNegativeCreateException() { @Test public void testNegativeLambdaParameter() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "interface Foo {", @@ -307,7 +375,7 @@ public void testNegativeLambdaParameter() { @Test public void testNegativeDoWhileLoop() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -328,7 +396,7 @@ public void testNegativeWhileLoop() { * people would prefer that in most cases. We could consider adding @Nullable if people would * find it useful. */ - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -345,7 +413,7 @@ public void testNegativeWhileLoop() { @Test public void testNegativeForLoop() { // Similar to testNegativeWhileLoop, @Nullable would be defensible here. - helper + aggressiveHelper .addSourceLines( "Foo.java", "class Foo {", @@ -356,4 +424,79 @@ public void testNegativeForLoop() { "}") .doTest(); } + + @Test + public void testNegativeCallArgNotNull() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " void foo(Integer i) {}", + " void bar() {", + " foo(1);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeCallAlreadyAnnotated() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import javax.annotation.Nullable;", + "class Foo {", + " void foo(@Nullable Integer i) {}", + " void bar() {", + " foo(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeCallTypeVariable() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " void foo(T t) {}", + " void bar() {", + " foo(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeCallOtherCompilationUnit() { + conservativeHelper + .addSourceLines( + "Foo.java", // + "class Foo {", + " void foo(Integer i) {}", + "}") + .addSourceLines( + "Bar.java", // + "class Bar {", + " void bar(Foo foo) {", + " foo.foo(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeCallVarargs() { + conservativeHelper + .addSourceLines( + "Foo.java", + "class Foo {", + " void foo(Integer... i) {}", + " void bar() {", + " foo(null, 1);", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java index 2118a709915..6fd7c5139ff 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/VoidMissingNullableTest.java @@ -27,14 +27,18 @@ /** {@link VoidMissingNullable}Test */ @RunWith(JUnit4.class) public class VoidMissingNullableTest { - private final CompilationTestHelper compilationHelper = + private final CompilationTestHelper conservativeCompilationHelper = CompilationTestHelper.newInstance(VoidMissingNullable.class, getClass()); - private final BugCheckerRefactoringTestHelper refactoringHelper = - BugCheckerRefactoringTestHelper.newInstance(VoidMissingNullable.class, getClass()); + private final CompilationTestHelper aggressiveCompilationHelper = + CompilationTestHelper.newInstance(VoidMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + private final BugCheckerRefactoringTestHelper aggressiveRefactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(VoidMissingNullable.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); @Test public void positive() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -51,7 +55,7 @@ public void positive() { @Test public void testDeclarationAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import javax.annotation.Nullable;", @@ -75,7 +79,7 @@ public void testDeclarationAnnotatedLocation() { @Test public void testTypeAnnotatedLocation() { - refactoringHelper + aggressiveRefactoringHelper .addInputLines( "in/Foo.java", "import org.checkerframework.checker.nullness.qual.Nullable;", @@ -99,7 +103,7 @@ public void testTypeAnnotatedLocation() { @Test public void negativeAlreadyAnnotated() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -114,7 +118,7 @@ public void negativeAlreadyAnnotated() { @Test public void negativeNotVoid() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -129,7 +133,7 @@ public void negativeNotVoid() { @Test public void positiveTypeArgument() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import java.util.List;", @@ -148,7 +152,7 @@ public void positiveTypeArgument() { @Test public void positiveTypeArgumentOtherAnnotation() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "NonNull.java", "import java.lang.annotation.ElementType;", @@ -176,7 +180,7 @@ public void positiveTypeArgumentOtherAnnotation() { @Test public void negativeTypeArgumentAlreadyAnnotated() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Nullable.java", "import java.lang.annotation.ElementType;", @@ -200,7 +204,7 @@ public void negativeTypeArgumentAlreadyAnnotated() { @Test public void negativeTypeArgumentAlreadyAnnotatedAnonymous() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Nullable.java", "import java.lang.annotation.ElementType;", @@ -223,7 +227,7 @@ public void negativeTypeArgumentAlreadyAnnotatedAnonymous() { @Test public void negativeTypeArgumentNotVoid() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Nullable.java", "import java.lang.annotation.ElementType;", @@ -247,7 +251,7 @@ public void negativeTypeArgumentNotVoid() { @Test public void negativeTypeArgumentDeclarationNullable() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import java.util.List;", @@ -263,7 +267,7 @@ public void negativeTypeArgumentDeclarationNullable() { @Test public void positiveLambdaParameter() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -278,7 +282,7 @@ public void positiveLambdaParameter() { @Test public void negativeLambdaParameterNoType() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -294,7 +298,7 @@ public void negativeLambdaParameterNoType() { @Test public void negativeVar() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -310,7 +314,7 @@ public void negativeVar() { @Test public void negativeOtherLocalVariable() { - compilationHelper + aggressiveCompilationHelper .addSourceLines( "Test.java", "import javax.annotation.Nullable;", @@ -323,4 +327,31 @@ public void negativeOtherLocalVariable() { "}") .doTest(); } + + @Test + public void positiveConservativeNullMarked() { + conservativeCompilationHelper + .addSourceLines( + "Test.java", + "import javax.annotation.Nullable;", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "class Test {", + " // BUG: Diagnostic contains: @Nullable", + " Void v;", + "}") + .doTest(); + } + + @Test + public void negativeConservativeNotNullMarked() { + conservativeCompilationHelper + .addSourceLines( + "Test.java", // + "import javax.annotation.Nullable;", + "class Test {", + " Void v;", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroNegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.java similarity index 56% rename from core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroNegativeCases.java rename to core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.java index 6ae7a8b9de3..fd5455c3c85 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroNegativeCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.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,10 +16,20 @@ package com.google.errorprone.bugpatterns.testdata; -/** @author cushon@google.com (Liam Miller-Cushon) */ -public class DivZeroNegativeCases { +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; - void method(int a) { - double y = (double) a / 0.0; +/** + * {@link BanJNDITest} + * + * @author tshadwell@google.com (Thomas Shadwell) + */ +class BanJNDIPositiveCases { + private static DirContext FakeDirContext = ((DirContext) new Object()); + + // Check we didn't ban all of Context by accident. + private void callsList() throws NamingException { + FakeDirContext.list(((Name) new Object())); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases.java new file mode 100644 index 00000000000..23c511d5e42 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases.java @@ -0,0 +1,149 @@ +/* + * 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.testdata; + +import java.io.IOException; +import java.util.Hashtable; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnector; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.sql.rowset.spi.SyncFactory; +import javax.sql.rowset.spi.SyncFactoryException; + +/** + * {@link BanJNDITest} + * + * @author tshadwell@google.com (Thomas Shadwell) + */ +class BanJNDIPositiveCases { + private static DirContext FakeDirContext = ((DirContext) new Object()); + + private void callsModifyAttributes() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.modifyAttributes(((Name) new Object()), 0, ((Attributes) new Object())); + } + + private void callsGetAttributes() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getAttributes(((Name) new Object())); + } + + private void callsSearch() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.search(((Name) new Object()), ((Attributes) new Object())); + } + + private void callsGetSchema() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getSchema(((Name) new Object())); + } + + private void callsGetSchemaClassDefinition() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getSchemaClassDefinition(((Name) new Object())); + } + + private static Context FakeContext = ((Context) new Object()); + + private void callsLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.lookup("hello"); + } + + private void callsSubclassLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.lookup("hello"); + } + + private void callsBind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.bind(((Name) new Object()), new Object()); + } + + private void subclassCallsBind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.bind(((Name) new Object()), new Object()); + } + + private void callsRebind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.rebind(((Name) new Object()), new Object()); + } + + private void subclassCallsRebind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.rebind(((Name) new Object()), new Object()); + } + + private void callsCreateSubcontext() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.createSubcontext((Name) new Object()); + } + + private void subclassCallsCreateSubcontext() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.createSubcontext((Name) new Object()); + } + + RMIConnector fakeRMIConnector = ((RMIConnector) new Object()); + + private void callsRMIConnect() throws IOException { + // BUG: Diagnostic contains: BanJNDI + fakeRMIConnector.connect(); + } + + private void callsEnumerateBindings() throws SyncFactoryException { + // BUG: Diagnostic contains: BanJNDI + SyncFactory.getInstance("fear is the little-death"); + } + + // unable to load javax.jdo for testing (must be some super optional pkg?) + + private void callsJMXConnectorFactoryConnect() throws IOException { + // BUG: Diagnostic contains: BanJNDI + JMXConnectorFactory.connect(((JMXServiceURL) new Object())); + } + + private void callsDoLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + InitialContext.doLookup(((Name) new Object())); + } + + private static boolean callToJMXConnectorFactoryConnect() + throws java.net.MalformedURLException, java.io.IOException { + JMXConnector connector = + // BUG: Diagnostic contains: BanJNDI + JMXConnectorFactory.connect( + new JMXServiceURL("service:jmx:rmi:///jndi/rmi:// fake data 123 ")); + connector.connect(); + + return false; + } + + private Object subclassesJavaNamingcontext() throws NamingException { + InitialContext c = new InitialContext(new Hashtable(0)); + // BUG: Diagnostic contains: BanJNDI + return c.lookup("hello"); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases_expected.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases_expected.java new file mode 100644 index 00000000000..a08ada492b1 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases_expected.java @@ -0,0 +1,169 @@ +/* + * 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.testdata; + +import com.google.security.annotations.SuppressBanJNDICompletedSecurityReview; +import java.io.IOException; +import java.util.Hashtable; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.rmi.RMIConnector; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.sql.rowset.spi.SyncFactory; +import javax.sql.rowset.spi.SyncFactoryException; + +/** + * {@link BanJNDITest} + * + * @author tshadwell@google.com (Thomas Shadwell) + */ +class BanJNDIPositiveCases { + private static DirContext FakeDirContext = ((DirContext) new Object()); + + @SuppressBanJNDICompletedSecurityReview + private void callsModifyAttributes() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.modifyAttributes(((Name) new Object()), 0, ((Attributes) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsGetAttributes() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getAttributes(((Name) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsSearch() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.search(((Name) new Object()), ((Attributes) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsGetSchema() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getSchema(((Name) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsGetSchemaClassDefinition() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.getSchemaClassDefinition(((Name) new Object())); + } + + private static Context FakeContext = ((Context) new Object()); + + @SuppressBanJNDICompletedSecurityReview + private void callsLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.lookup("hello"); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsSubclassLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.lookup("hello"); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsBind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.bind(((Name) new Object()), new Object()); + } + + @SuppressBanJNDICompletedSecurityReview + private void subclassCallsBind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.bind(((Name) new Object()), new Object()); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsRebind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.rebind(((Name) new Object()), new Object()); + } + + @SuppressBanJNDICompletedSecurityReview + private void subclassCallsRebind() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.rebind(((Name) new Object()), new Object()); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsCreateSubcontext() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeContext.createSubcontext((Name) new Object()); + } + + @SuppressBanJNDICompletedSecurityReview + private void subclassCallsCreateSubcontext() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + FakeDirContext.createSubcontext((Name) new Object()); + } + + RMIConnector fakeRMIConnector = ((RMIConnector) new Object()); + + @SuppressBanJNDICompletedSecurityReview + private void callsRMIConnect() throws IOException { + // BUG: Diagnostic contains: BanJNDI + fakeRMIConnector.connect(); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsEnumerateBindings() throws SyncFactoryException { + // BUG: Diagnostic contains: BanJNDI + SyncFactory.getInstance("fear is the little-death"); + } + + // unable to load javax.jdo for testing (must be some super optional pkg?) + + @SuppressBanJNDICompletedSecurityReview + private void callsJMXConnectorFactoryConnect() throws IOException { + // BUG: Diagnostic contains: BanJNDI + JMXConnectorFactory.connect(((JMXServiceURL) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private void callsDoLookup() throws NamingException { + // BUG: Diagnostic contains: BanJNDI + InitialContext.doLookup(((Name) new Object())); + } + + @SuppressBanJNDICompletedSecurityReview + private static boolean callToJMXConnectorFactoryConnect() + throws java.net.MalformedURLException, java.io.IOException { + JMXConnector connector = + // BUG: Diagnostic contains: BanJNDI + JMXConnectorFactory.connect( + new JMXServiceURL("service:jmx:rmi:///jndi/rmi:// fake data 123 ")); + connector.connect(); + + return false; + } + + @SuppressBanJNDICompletedSecurityReview + private Object subclassesJavaNamingcontext() throws NamingException { + InitialContext c = new InitialContext(new Hashtable(0)); + // BUG: Diagnostic contains: BanJNDI + return c.lookup("hello"); + } +} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroPositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroPositiveCases.java deleted file mode 100644 index 7f8ed5c5b71..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroPositiveCases.java +++ /dev/null @@ -1,87 +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.testdata; - -/** @author cushon@google.com (Liam Miller-Cushon) */ -public class DivZeroPositiveCases { - - public static void main(String[] args) { - new DivZeroPositiveCases(); - } - - static int staticOne = 0; - - // BUG: Diagnostic contains: DivZero - static int staticTwo = staticOne / 0; - - // BUG: Diagnostic contains: DivZero - static int staticThree = (staticTwo / 0); - - int fieldOne; - - // BUG: Diagnostic contains: DivZero - int fieldTwo = fieldOne / 0; - - // BUG: Diagnostic contains: DivZero - int fieldThree = (fieldTwo /= 0); - - int f() { - return 42; - } - - void method(final int a, double b, boolean flag) { - int x; - double y; - - // BUG: Diagnostic contains: throw new ArithmeticException - x = a / 0; - - // BUG: Diagnostic contains: throw new ArithmeticException - x /= 0; - - x = - ((((a / a) / (a / a)) / ((a / a) / (a / a))) / (((a / a) / (a / a)) / ((a / a) / (a / a)))) - / - // BUG: Diagnostic contains: zero - ((((a / a) / (a / a)) / ((a / 0) / (a / a))) - / (((a / a) / (a / a)) / ((a / a) / (a / a)))); - - // BUG: Diagnostic contains: throw new ArithmeticException - x = flag ? a / 0 : 42; - - // BUG: Diagnostic contains: throw new ArithmeticException - for (int i = 0; i < 10; i /= 0) {} - - Object o = - new Object() { - // BUG: Diagnostic contains: throw new ArithmeticException - int x = a / 0; - }; - - // BUG: Diagnostic contains: throw new ArithmeticException - x = f() / 0; - } - - // TODO(cushon): write a check for self-references via qualified names in field initializers, - // even if JLS 8.3.2.3 permits it. - - // BUG: Diagnostic contains: DivZero - int selfRefField = this.selfRefField / 0; - - // BUG: Diagnostic contains: DivZero - static int staticSelfRefField = DivZeroPositiveCases.staticSelfRefField / 0; -} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java index e35a7e3db01..16a977fc19a 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByCheckerTest.java @@ -1043,11 +1043,8 @@ public void tryWithResources() { .doTest(); } - // Test that the contents of try-with-resources block are ignored (for now), but the catch and - // finally blocks are checked. - // TODO(cushon): support try-with-resources block. @Test - public void tryWithResourcesAreNotFullyUnsupported() { + public void tryWithResources_resourceVariables() { compilationHelper .addSourceLines( "threadsafety/Test.java", @@ -1060,7 +1057,8 @@ public void tryWithResourcesAreNotFullyUnsupported() { " int x;", " void m(AutoCloseable c) throws Exception {", " try (AutoCloseable unused = c) {", - " x++; // should be an error!", + " // BUG: Diagnostic contains:", + " x++;", " } catch (Exception e) {", " // BUG: Diagnostic contains:", " // should be guarded by 'this.lock'", diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java index c09afccb0c7..864524e5e8e 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java @@ -374,6 +374,52 @@ public void extendsImmutableAnnotated_mutableBounds() { .doTest(); } + @Test + public void withinMutableClass() { + compilationHelper + .addSourceLines( + "A.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class A {", + " List xs = new ArrayList<>();", + " // BUG: Diagnostic contains: has mutable enclosing instance", + " @Immutable class B {", + " int get() {", + " return xs.get(0);", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void localClassCapturingMutableState() { + compilationHelper + .addSourceLines( + "A.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "@Immutable", + "class A {", + " @Immutable interface B { int get(); }", + " void test() {", + " List xs = new ArrayList<>();", + " @Immutable", + " // BUG: Diagnostic contains: but 'List' is mutable", + " class C implements B {", + " @Override", + " public int get() {", + " return xs.get(0);", + " }", + " }", + " }", + "}") + .doTest(); + } + @Test public void typeParameterWithImmutableBound() { compilationHelper @@ -2525,6 +2571,25 @@ public void lambda_canCloseAroundImmutableField() { .doTest(); } + @Test + public void lambda_cannotCloseAroundMutableFieldQualifiedWithThis() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " private int b = 1;", + " void test(ImmutableFunction f) {", + " // BUG: Diagnostic contains:", + " test(x -> this.b);", + " }", + "}") + .doTest(); + } + @Test public void lambda_cannotCloseAroundMutableLocal() { compilationHelper @@ -2578,6 +2643,24 @@ public void lambda_canHaveMutableVariablesWithin() { .doTest(); } + @Test + public void lambda_canAccessStaticField() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " static class A {", + " public static int FOO = 1;", + " }", + " void test(ImmutableFunction f) {", + " test(x -> A.FOO);", + " }", + "}") + .doTest(); + } + @Test public void lambda_cannotCallMethodOnMutableClass() { compilationHelper @@ -2621,7 +2704,7 @@ public void lambda_canCallMethodOnImmutableClass() { } @Test - public void subclassesOfMutableType() { + public void checksEffectiveTypeOfReceiver() { compilationHelper .addSourceLines( "Test.java", @@ -2631,14 +2714,259 @@ public void subclassesOfMutableType() { "abstract class Test {", " @Immutable interface ImmutableFunction extends Function {", " default ImmutableFunction andThen(ImmutableFunction fn) {", - // TODO(ghm): this one is sad, we're really accessing an immutable class's method here, - // but the owner of the method is not @Immutable. Look for a better heuristic to find - // the receiver type. - " // BUG: Diagnostic contains:", " return x -> fn.apply(apply(x));", " }", " }", "}") .doTest(); } + + @Test + public void checksEffectiveTypeOfReceiver_whenNotDirectOuterClass() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.function.Function;", + "@Immutable", + "abstract class Test implements Function {", + " @Immutable interface ImmutableFunction { String apply(String a); }", + " class A {", + " ImmutableFunction asImmutable() {", + " return x -> apply(x);", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_onImmutableType() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.collect.ImmutableMap;", + "import com.google.errorprone.annotations.Immutable;", + "abstract class Test {", + " @Immutable interface ImmutableFunction { String apply(String b); }", + " void test(ImmutableFunction f) {", + " ImmutableMap map = ImmutableMap.of();", + " test(map::get);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_onMutableType() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.HashMap;", + "import java.util.Map;", + "abstract class Test {", + " @Immutable interface ImmutableFunction { String apply(String b); }", + " void test(ImmutableFunction f) {", + " Map map = new HashMap<>();", + " // BUG: Diagnostic contains:", + " test(map::get);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_onExpressionWithMutableType() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.collect.Maps;", + "import com.google.errorprone.annotations.Immutable;", + "abstract class Test {", + " @Immutable interface ImmutableFunction { String apply(String b); }", + " void test(ImmutableFunction f) {", + " // BUG: Diagnostic contains:", + " test(Maps.newHashMap()::get);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_toStaticMethod() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.collect.Lists;", + "import com.google.errorprone.annotations.Immutable;", + "abstract class Test {", + " @Immutable interface ImmutableProvider { Object get(); }", + " void test(ImmutableProvider f) {", + " test(Lists::newArrayList);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_toUnboundMethodReference() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.Set;", + "abstract class Test {", + " @Immutable interface ImmutableBiConsumer { void accept(Set xs, String x); }", + " void test(ImmutableBiConsumer c) {", + " test(Set::add);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_toConstructor() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "abstract class Test {", + " @Immutable interface ImmutableProvider { Object get(); }", + " void test(ImmutableProvider f) {", + " test(ArrayList::new);", + " }", + "}") + .doTest(); + } + + @Test + public void methodReference_immutableTypeParam() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.ImmutableTypeParameter;", + "import java.util.ArrayList;", + "abstract class Test {", + " interface ImmutableProvider<@ImmutableTypeParameter T> { T get(); }", + " void test(ImmutableProvider f) {", + " // BUG: Diagnostic contains:", + " test(ArrayList::new);", + " }", + "}") + .doTest(); + } + + @Test + public void lambda_immutableTypeParam() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.ImmutableTypeParameter;", + "import java.util.ArrayList;", + "abstract class Test {", + " interface ImmutableProvider<@ImmutableTypeParameter T> { T get(); }", + " void test(ImmutableProvider f) {", + " // BUG: Diagnostic contains:", + " test(() -> new ArrayList<>());", + " }", + "}") + .doTest(); + } + + @Test + public void chainedGettersAreAcceptable() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.ArrayList;", + "import java.util.List;", + "class Test {", + " final Test t = null;", + " final List xs = new ArrayList<>();", + " final List getXs() {", + " return xs;", + " }", + " @Immutable interface ImmutableFunction { String apply(String b); }", + " void test(ImmutableFunction f) {", + " test(x -> {", + " Test t = new Test();", + " return t.xs.get(0) + t.getXs().get(0) + t.t.t.xs.get(0);", + " });", + " }", + "}") + .doTest(); + } + + @Test + public void anonymousClass_cannotCloseAroundMutableLocal() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.List;", + "import java.util.ArrayList;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " void test(ImmutableFunction f) {", + " List xs = new ArrayList<>();", + " // BUG: Diagnostic contains:", + " test(new ImmutableFunction<>() {", + " @Override public Integer apply(Integer x) {", + " return xs.get(x);", + " }", + " });", + " }", + "}") + .doTest(); + } + + @Test + public void anonymousClass_hasMutableFieldSuppressed_noWarningAtUsageSite() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.List;", + "import java.util.ArrayList;", + "class Test {", + " @Immutable interface ImmutableFunction { A apply(B b); }", + " void test(ImmutableFunction f) {", + " test(new ImmutableFunction<>() {", + " @Override public Integer apply(Integer x) {", + " return xs.get(x);", + " }", + " @SuppressWarnings(\"Immutable\")", + " List xs = new ArrayList<>();", + " });", + " }", + "}") + .doTest(); + } + + @Test + public void anonymousClass_canCallSuperMethodOnNonImmutableSuperClass() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.Immutable;", + "import java.util.List;", + "import java.util.ArrayList;", + "class Test {", + " interface Function { default void foo() {} }", + " @Immutable interface ImmutableFunction extends Function { A apply(B b);" + + " }", + " void test(ImmutableFunction f) {", + " test(new ImmutableFunction<>() {", + " @Override public Integer apply(Integer x) {", + " foo();", + " return 0;", + " }", + " });", + " }", + "}") + .doTest(); + } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodCheckerTest.java deleted file mode 100644 index 6a999b85d47..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodCheckerTest.java +++ /dev/null @@ -1,177 +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.errorprone.CompilationTestHelper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** {@link LockMethodChecker}Test */ -@RunWith(JUnit4.class) -public class LockMethodCheckerTest { - private final CompilationTestHelper compilationHelper = - CompilationTestHelper.newInstance(LockMethodChecker.class, getClass()); - - @Test - public void testLocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " @LockMethod({\"lock1\", \"lock2\"})", - " void m() {", - " lock1.lock();", - " lock2.lock();", - " }", - "}") - .doTest(); - } - - @Test - public void testLockedAndUnlocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " // BUG: Diagnostic contains: not acquired: this.lock1, this.lock2", - " @LockMethod({\"lock1\", \"lock2\"}) void m() {", - " lock1.lock();", - " lock2.lock();", - " lock1.unlock();", - " lock2.unlock();", - " }", - "}") - .doTest(); - } - - @Test - public void testLockedRWLock() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import java.util.concurrent.locks.ReentrantReadWriteLock;", - "class Test {", - " ReentrantReadWriteLock lock;", - " @LockMethod(\"lock\")", - " void m() {", - " lock.readLock().lock();", - " }", - " @LockMethod(\"lock\")", - " void n() {", - " lock.writeLock().lock();", - " }", - "}") - .doTest(); - } - - @Test - public void testLockedMonitor() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import com.google.common.util.concurrent.Monitor;", - "class Test {", - " Monitor monitor;", - " @LockMethod(\"monitor\")", - " void m() {", - " monitor.enter();", - " }", - "}") - .doTest(); - } - - @Test - public void testNotLocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " // BUG: Diagnostic contains: not acquired: this.lock1, this.lock2", - " @LockMethod({\"lock1\", \"lock2\"}) void m() {}", - "}") - .doTest(); - } - - @Test - public void testNotLockedRWLock() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import java.util.concurrent.locks.ReentrantReadWriteLock;", - "class Test {", - " ReentrantReadWriteLock lock;", - " // BUG: Diagnostic contains: not acquired: this.lock", - " @LockMethod(\"lock\") void n() {}", - "}") - .doTest(); - } - - @Test - public void testNotLockedMonitor() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "import com.google.common.util.concurrent.Monitor;", - "class Test {", - " Monitor monitor;", - " // BUG: Diagnostic contains: not acquired: this.monitor", - " @LockMethod(\"monitor\") void m() {}", - "}") - .doTest(); - } - - @Test - public void testBadLockExpression() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.LockMethod;", - "class Test {", - " // BUG: Diagnostic contains: Could not resolve lock expression.", - " @LockMethod(\"mu\") void m() {}", - "}") - .doTest(); - } -} diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodCheckerTest.java deleted file mode 100644 index c0d7f71ae37..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodCheckerTest.java +++ /dev/null @@ -1,176 +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.errorprone.CompilationTestHelper; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** {@link UnlockMethodChecker}Test */ -@RunWith(JUnit4.class) -public class UnlockMethodCheckerTest { - private final CompilationTestHelper compilationHelper = - CompilationTestHelper.newInstance(UnlockMethodChecker.class, getClass()); - - @Test - public void testUnlocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " @UnlockMethod({\"lock1\", \"lock2\"}) void m() {", - " lock1.unlock();", - " lock2.unlock();", - " }", - "}") - .doTest(); - } - - @Test - public void testUnlockedAndLocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " // BUG: Diagnostic contains: not released: this.lock1, this.lock2", - " @UnlockMethod({\"lock1\", \"lock2\"}) void m() {", - " lock1.unlock();", - " lock2.unlock();", - " lock1.lock();", - " lock2.lock();", - " }", - "}") - .doTest(); - } - - @Test - public void testUnlockedRWLock() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import java.util.concurrent.locks.ReentrantReadWriteLock;", - "class Test {", - " ReentrantReadWriteLock lock;", - " @UnlockMethod(\"lock\")", - " void m() {", - " lock.readLock().unlock();", - " }", - " @UnlockMethod(\"lock\")", - " void n() {", - " lock.writeLock().unlock();", - " }", - "}") - .doTest(); - } - - @Test - public void testUnlockedMonitor() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import com.google.common.util.concurrent.Monitor;", - "class Test {", - " Monitor monitor;", - " @UnlockMethod(\"monitor\")", - " void m() {", - " monitor.leave();", - " }", - "}") - .doTest(); - } - - @Test - public void testNotUnlocked() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import javax.annotation.concurrent.GuardedBy;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import java.util.concurrent.locks.Lock;", - "class Test {", - " Lock lock1;", - " Lock lock2;", - " // BUG: Diagnostic contains: not released: this.lock1, this.lock2", - " @UnlockMethod({\"lock1\", \"lock2\"}) void m() {}", - "}") - .doTest(); - } - - @Test - public void testNotUnlockedRWLock() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import java.util.concurrent.locks.ReentrantReadWriteLock;", - "class Test {", - " ReentrantReadWriteLock lock;", - " // BUG: Diagnostic contains: not released: this.lock", - " @UnlockMethod(\"lock\") void n() {}", - "}") - .doTest(); - } - - @Test - public void testNotUnlockedMonitor() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "import com.google.common.util.concurrent.Monitor;", - "class Test {", - " Monitor monitor;", - " // BUG: Diagnostic contains: not released: this.monitor", - " @UnlockMethod(\"monitor\") void m() {}", - "}") - .doTest(); - } - - @Test - public void testBadLockExpression() { - compilationHelper - .addSourceLines( - "threadsafety/Test.java", - "package threadsafety;", - "import com.google.errorprone.annotations.concurrent.UnlockMethod;", - "class Test {", - " // BUG: Diagnostic contains: Could not resolve lock expression.", - " @UnlockMethod(\"mu\") void m() {}", - "}") - .doTest(); - } -} diff --git a/core/src/test/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcherTest.java b/core/src/test/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcherTest.java index 1b6922cb79c..22fd519e5e6 100644 --- a/core/src/test/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcherTest.java +++ b/core/src/test/java/com/google/errorprone/matchers/CompileTimeConstantExpressionMatcherTest.java @@ -199,6 +199,21 @@ public void conditionalExpression() { .doTest(); } + @Test + public void parentheses() { + testHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.CompileTimeConstant;", + "abstract class Test {", + " public void m(@CompileTimeConstant String ctc) {", + " // BUG: Diagnostic contains: true", + " String a = (ctc);", + " }", + "}") + .doTest(); + } + @Test public void concatenatedStrings() { testHelper @@ -216,6 +231,12 @@ public void concatenatedStrings() { " String c = nonCtc + \"foo\";", " // BUG: Diagnostic contains: false", " String d = nonCtc + ctc;", + " // BUG: Diagnostic contains: true", + " String e = \"foo\" + (ctc == null ? \"\" : \"\");", + " // BUG: Diagnostic contains: true", + " String f = \"foo\" + 1;", + " // BUG: Diagnostic contains: true", + " String g = \"foo\" + 3.14;", " }", "}") .doTest(); diff --git a/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java b/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java index f20ae9a8ad4..c928231983e 100644 --- a/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java +++ b/core/src/test/java/com/google/errorprone/refaster/TemplateIntegrationTest.java @@ -234,6 +234,11 @@ public void expressionPlaceholderAllowsIdentity() throws IOException { runTest("PlaceholderAllowsIdentityTemplate"); } + @Test + public void voidExpressionPlaceholder() throws IOException { + runTest("VoidExpressionPlaceholderTemplate"); + } + @Test public void blockPlaceholder() throws IOException { runTest("BlockPlaceholderTemplate"); diff --git a/core/src/test/java/com/google/errorprone/refaster/testdata/input/VoidExpressionPlaceholderTemplateExample.java b/core/src/test/java/com/google/errorprone/refaster/testdata/input/VoidExpressionPlaceholderTemplateExample.java new file mode 100644 index 00000000000..9128c43bdc1 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/input/VoidExpressionPlaceholderTemplateExample.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.refaster.testdata; + +import java.util.List; + +/** Test data for {@code VoidExpressionPlaceholderTemplate}. */ +public class VoidExpressionPlaceholderTemplateExample { + public static void foo(String s) { + s.length(); + } + + public void positiveExample(List list) { + list.stream().forEach(x -> foo(x)); + } +} diff --git a/core/src/test/java/com/google/errorprone/refaster/testdata/output/VoidExpressionPlaceholderTemplateExample.java b/core/src/test/java/com/google/errorprone/refaster/testdata/output/VoidExpressionPlaceholderTemplateExample.java new file mode 100644 index 00000000000..234920701eb --- /dev/null +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/output/VoidExpressionPlaceholderTemplateExample.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.refaster.testdata; + +import java.util.List; + +/** Test data for {@code VoidExpressionPlaceholderTemplate}. */ +public class VoidExpressionPlaceholderTemplateExample { + public static void foo(String s) { + s.length(); + } + + public void positiveExample(List list) { + list.forEach(x->foo(x)); + } +} diff --git a/core/src/test/java/com/google/errorprone/refaster/testdata/template/VoidExpressionPlaceholderTemplate.java b/core/src/test/java/com/google/errorprone/refaster/testdata/template/VoidExpressionPlaceholderTemplate.java new file mode 100644 index 00000000000..00c10f00df4 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/refaster/testdata/template/VoidExpressionPlaceholderTemplate.java @@ -0,0 +1,38 @@ +/* + * 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.refaster.testdata.template; + +import com.google.errorprone.refaster.annotation.AfterTemplate; +import com.google.errorprone.refaster.annotation.BeforeTemplate; +import com.google.errorprone.refaster.annotation.Placeholder; +import java.util.Collection; + +/** Test case with a void placeholder method that is used as an expression. */ +public abstract class VoidExpressionPlaceholderTemplate { + @Placeholder + abstract void consume(T t); + + @BeforeTemplate + void before(Collection collection) { + collection.stream().forEach(x -> consume(x)); + } + + @AfterTemplate + void after(Collection collection) { + collection.forEach(x -> consume(x)); + } +} diff --git a/core/src/test/java/com/google/errorprone/scanner/ScannerSupplierTest.java b/core/src/test/java/com/google/errorprone/scanner/ScannerSupplierTest.java index 0fa66b2a940..38c4c176a6f 100644 --- a/core/src/test/java/com/google/errorprone/scanner/ScannerSupplierTest.java +++ b/core/src/test/java/com/google/errorprone/scanner/ScannerSupplierTest.java @@ -44,9 +44,9 @@ import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.ChainingConstructorIgnoresParameter; import com.google.errorprone.bugpatterns.DepAnn; -import com.google.errorprone.bugpatterns.DivZero; import com.google.errorprone.bugpatterns.EqualsIncompatibleType; import com.google.errorprone.bugpatterns.LongLiteralLowerCaseSuffix; +import com.google.errorprone.bugpatterns.MethodCanBeStatic; import com.google.errorprone.bugpatterns.PackageLocation; import com.google.errorprone.bugpatterns.ReferenceEquality; import com.google.errorprone.bugpatterns.StaticQualifiedUsingExpression; @@ -325,7 +325,9 @@ public void applyOverridesEnableAllChecks() { // The 'AllDisabledChecksAsWarnings' flag doesn't populate through to additional plugins assertScanner( ss.applyOverrides(epOptions) - .plus(ScannerSupplier.fromBugCheckerClasses(DivZero.class).filter(t -> false))) + .plus( + ScannerSupplier.fromBugCheckerClasses(MethodCanBeStatic.class) + .filter(t -> false))) .hasEnabledChecks(BadShiftAmount.class, StaticQualifiedUsingExpression.class); } diff --git a/core/src/test/proto/proto_test.proto b/core/src/test/proto/proto_test.proto index 0daf521250f..3f4d6bc2718 100644 --- a/core/src/test/proto/proto_test.proto +++ b/core/src/test/proto/proto_test.proto @@ -30,6 +30,8 @@ message TestProtoMessage { optional int32 test_field_named_count = 3; optional bool boolean_field = 4; map weight = 5; + /* Named ending in "builder" to check the Java builder API */ + optional TestFieldProtoMessage foo_builder = 6; extensions 100 to 199; } diff --git a/docgen/pom.xml b/docgen/pom.xml index a57b821986f..8d2f90bf630 100644 --- a/docgen/pom.xml +++ b/docgen/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 Documentation tool for generating Error Prone bugpattern documentation diff --git a/docgen/src/main/java/com/google/errorprone/DocGenTool.java b/docgen/src/main/java/com/google/errorprone/DocGenTool.java index 4ab0f742e1d..b5cfe1f3638 100644 --- a/docgen/src/main/java/com/google/errorprone/DocGenTool.java +++ b/docgen/src/main/java/com/google/errorprone/DocGenTool.java @@ -86,7 +86,7 @@ public Target convert(String arg) { public static void main(String[] args) throws IOException { Options options = new Options(); - JCommander unused = new JCommander(options, args); + new JCommander(options).parse(args); Path bugPatterns = Paths.get(options.bugPatterns); if (!Files.exists(bugPatterns)) { diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml index 42915547130..56f75c1f4ce 100644 --- a/docgen_processor/pom.xml +++ b/docgen_processor/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 JSR-269 annotation processor for @BugPattern annotation diff --git a/docs/bugpattern/BanJNDI.md b/docs/bugpattern/BanJNDI.md new file mode 100644 index 00000000000..66c99e9c3f9 --- /dev/null +++ b/docs/bugpattern/BanJNDI.md @@ -0,0 +1,64 @@ +JNDI ("Java Naming and Directory Interface") is a Java JDK API representing an +abstract directory service such as DNS, a file system or LDAP. Critically, JNDI +allows Java objects to be serialized and deserialized on the wire in +implementing systems. This means that if a Java application is allowed to +perform a JNDI lookup over some transport protocol then the server it connects +to can execute arbitrary attacker-defined code. See +[this](https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf) +Black Hat talk for more information. + +This checker bans usage of every API in the Java JDK that can result in +deserialising an unsafe object via JNDI. The list of APIs is generated from +static callgraph analysis of the JDK, rooted at `javax.naming.Context.lookup` +and is as follows: + +- `javax.naming.Context.lookup` +- `javax.jdo.JDOHelper.getPersistenceManagerFactory` +- `javax.jdo.JDOHelperTest.testGetPMFBadJNDI` +- `javax.jdo.JDOHelperTest.testGetPMFBadJNDIGoodClassLoader` +- `javax.jdo.JDOHelperTest.testGetPMFNullJNDI` +- `javax.jdo.JDOHelperTest.testGetPMFNullJNDIGoodClassLoader` +- `javax.management.remote.JMXConnectorFactory.connect` +- `javax.management.remote.rmi.RMIConnector.connect` +- `javax.management.remote.rmi.RMIConnector.findRMIServer` +- `javax.management.remote.rmi.RMIConnector.findRMIServerJNDI` +- `javax.management.remote.rmi.RMIConnector.RMIClientCommunicatorAdmin.doStart` +- `javax.naming.directory.InitialDirContext.bind` +- `javax.naming.directory.InitialDirContext.createSubcontext` +- `javax.naming.directory.InitialDirContext.getAttributes` +- `javax.naming.directory.InitialDirContext.getSchema` +- `javax.naming.directory.InitialDirContext.getSchemaClassDefinition` +- `javax.naming.directory.InitialDirContext.modifyAttributes` +- `javax.naming.directory.InitialDirContext.rebind` +- `javax.naming.directory.InitialDirContext.search` +- `javax.naming.InitialContext.doLookup` +- `javax.naming.InitialContext.lookup` +- `javax.naming.spi.ContinuationContext.lookup` +- `javax.naming.spi.ContinuationDirContext.bind` +- `javax.naming.spi.ContinuationDirContext.createSubcontext` +- `javax.naming.spi.ContinuationDirContext.getAttributes` +- `javax.naming.spi.ContinuationDirContext.getSchema` +- `javax.naming.spi.ContinuationDirContext.getSchemaClassDefinition` +- `javax.naming.spi.ContinuationDirContext.getTargetContext` +- `javax.naming.spi.ContinuationDirContext.modifyAttributes` +- `javax.naming.spi.ContinuationDirContext.rebind` +- `javax.naming.spi.ContinuationDirContext.search` +- `javax.sql.rowset.spi.ProviderImpl.getDataSourceLock` +- `javax.sql.rowset.spi.ProviderImpl.getProviderGrade` +- `javax.sql.rowset.spi.ProviderImpl.getRowSetReader` +- `javax.sql.rowset.spi.ProviderImpl.getRowSetWriter` +- `javax.sql.rowset.spi.ProviderImpl.setDataSourceLock` +- `javax.sql.rowset.spi.ProviderImpl.supportsUpdatableView` +- `javax.sql.rowset.spi.SyncFactory.enumerateBindings` +- `javax.sql.rowset.spi.SyncFactory.getInstance` +- `javax.sql.rowset.spi.SyncFactory.initJNDIContext` +- `javax.sql.rowset.spi.SyncFactory.parseJNDIContext` + +A small subset of these are banned directly. The rest are banned indirectly by +banning the `lookup()`, `bind()`, `rebind()`, `getAttributes()`, +`modifyAttriutes()`, `createSubcontext()`, `getSchema()`, +`getSchemaClassDefinition()` and `search()` methods on any subclass +(implementer) of `javax.naming.Context`. The indirect ban is necessary due to +these methods being vulnerable in previously noted subclasses in the JDK. If +they were not banned at the Context level, a cast to Context would make the +vulnerable call invisible to static analysis. diff --git a/docs/bugpattern/ComparableType.md b/docs/bugpattern/ComparableType.md index cde971d8d92..c2f0d011fb0 100644 --- a/docs/bugpattern/ComparableType.md +++ b/docs/bugpattern/ComparableType.md @@ -13,7 +13,7 @@ not this: ```java class Foo implements Comparable { - public int compareTo(Foo other) { ... } + public int compareTo(Bar other) { ... } } ``` diff --git a/docs/bugpattern/DivZero.md b/docs/bugpattern/DivZero.md deleted file mode 100644 index 6e1cd396afc..00000000000 --- a/docs/bugpattern/DivZero.md +++ /dev/null @@ -1 +0,0 @@ -This code will cause a runtime arithmetic exception if it is executed. diff --git a/docs/bugpattern/Immutable.md b/docs/bugpattern/Immutable.md index 9e3c8d514e6..5fbc0a45795 100644 --- a/docs/bugpattern/Immutable.md +++ b/docs/bugpattern/Immutable.md @@ -3,7 +3,7 @@ annotation (`com.google.errorprone.annotations.Immutable`) are deeply immutable. It also checks that any class extending an `@Immutable`-annotated class or implementing an `@Immutable`-annotated interface are also immutable. -Other versions of the annotation, such as +NOTE: Other versions of the annotation, such as `javax.annotation.concurrent.Immutable`, are currently *not* enforced. An object is immutable if its state cannot be observed to change after diff --git a/docs/bugpattern/Interruption.md b/docs/bugpattern/Interruption.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/bugpattern/LockMethodChecker.md b/docs/bugpattern/LockMethodChecker.md deleted file mode 100644 index 93baa0aa22e..00000000000 --- a/docs/bugpattern/LockMethodChecker.md +++ /dev/null @@ -1,2 +0,0 @@ -Methods with the @LockMethod annotation are expected to acquire one or more -locks. The caller will hold the locks when the function finishes execution. diff --git a/docs/bugpattern/RemoveUnusedImports.md b/docs/bugpattern/RemoveUnusedImports.md index 42e034d3bca..213e63b34ac 100644 --- a/docs/bugpattern/RemoveUnusedImports.md +++ b/docs/bugpattern/RemoveUnusedImports.md @@ -1 +1,33 @@ This import is unused. + +## The check reported an import that was actually used! + +The check has no known bugs. If it reports an unused import that looks like it +was actually used, try removing it and recompiling. If everything still +compiles, it was unused. + +Note that the check can detect some unused imports that `google-java-format` +cannot. The formatter looks at a single file at a time, so `RemoveUnusedImports` +is more accurate in examples like the following: + +```java +package a; + +import b.Baz; // this is unused! + +class Foo extends Bar { + Baz baz; // this is a.Bar.Baz (from the supertype), *not* b.Baz +} +``` + +```java +package a; +class Bar { + class Baz {} +} +``` + +```java +package b; +class Baz {} +``` diff --git a/docs/bugpattern/SystemExitOutsideMain.md b/docs/bugpattern/SystemExitOutsideMain.md index ca9c62660a8..4fc6e89d523 100644 --- a/docs/bugpattern/SystemExitOutsideMain.md +++ b/docs/bugpattern/SystemExitOutsideMain.md @@ -10,16 +10,17 @@ For example, prefer this: ```java public static void main(String[] args) { try { - doSomething(args); + doSomething(args[0]); } catch (MyUncheckedException e) { System.err.println(e.getMessage()); System.exit(1); } } -private static void doSomething(args) { +// In library code +public static void doSomething(String s) { try { - doSomethingElse(...); + doSomethingElse(s); } catch (MyCheckedException e) { throw new MyUncheckedException(e); } @@ -30,8 +31,13 @@ to this: ```java public static void main(String[] args) { + doSomething(args[0]); +} + +// In library code +public static void doSomething(String s) { try { - doSomething(...); + doSomethingElse(s) } catch (MyCheckedException e) { System.err.println(e.getMessage()); System.exit(1); diff --git a/docs/bugpattern/UnlockMethod.md b/docs/bugpattern/UnlockMethod.md deleted file mode 100644 index d523e33ccfd..00000000000 --- a/docs/bugpattern/UnlockMethod.md +++ /dev/null @@ -1,3 +0,0 @@ -Methods with the @UnlockMethod annotation are expected to release one or more -locks. The caller must hold the locks when the function is entered, and will not -hold them when it completes. diff --git a/docs/bugpattern/UnnecessaryDefaultInEnumSwitch.md b/docs/bugpattern/UnnecessaryDefaultInEnumSwitch.md index 8702dc52ae5..1f25bb30fbf 100644 --- a/docs/bugpattern/UnnecessaryDefaultInEnumSwitch.md +++ b/docs/bugpattern/UnnecessaryDefaultInEnumSwitch.md @@ -51,10 +51,10 @@ This check should be used together with `MissingCasesInEnumSwitch` in environments where that kind of binary incompatibility is very unlikely. For example, if your build system accurately tracks changes to dependencies and you are deploying an application (instead of a library), the risk of skew between -compile-time and runtime is minimal. One the other hand, if you are a library -author and your code switches on an enum in a different library, you want want -to include 'defensive' default cases to handle the situation where a user -deploys your code together with an incompatible version of the other library. +compile-time and runtime is minimal. On the other hand, if you are a library +author and your code switches on an enum in a different library, you want to +include 'defensive' default cases to handle the situation where a user deploys +your code together with an incompatible version of the other library. [JLS §14.21]: https://docs.oracle.com/javase/specs/jls/se10/html/jls-14.html#jls-14.21 [JLS §13.4.26]: https://docs.oracle.com/javase/specs/jls/se10/html/jls-13.html#jls-13.4.26 diff --git a/docs/bugpattern/WrongOneof.md b/docs/bugpattern/WrongOneof.md new file mode 100644 index 00000000000..9362527fb8c --- /dev/null +++ b/docs/bugpattern/WrongOneof.md @@ -0,0 +1,11 @@ +When switching over a proto `one_of`, getters that don't match the current case +are guaranteed to be return a default instance: + +```java +switch (foo.getBlahCase()) { + case FOO: + return foo.getFoo(); + case BAR: + return foo.getFoo(); // should be foo.getBar() +} +``` diff --git a/pom.xml b/pom.xml index ecf68575fa5..71f205da709 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ Error Prone parent POM com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 pom Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. @@ -45,6 +45,7 @@ 1.6.8 3.19.2 1.43.2 + 0.2.0 @@ -248,6 +249,7 @@ ossrh https://oss.sonatype.org/ + true diff --git a/refaster/pom.xml b/refaster/pom.xml index d7951f0e492..d9d988df7ea 100644 --- a/refaster/pom.xml +++ b/refaster/pom.xml @@ -19,7 +19,7 @@ error_prone_parent com.google.errorprone - HEAD-SNAPSHOT + 2.14.0 4.0.0 diff --git a/test_helpers/pom.xml b/test_helpers/pom.xml index 6d798ac870c..6c69bfa8ede 100644 --- a/test_helpers/pom.xml +++ b/test_helpers/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 error-prone test helpers diff --git a/type_annotations/pom.xml b/type_annotations/pom.xml index b2495a2167f..b519761b34e 100644 --- a/type_annotations/pom.xml +++ b/type_annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.14.0 error-prone type annotations