From 8bee38e4e37915863585ab030e7a33f4e053aa14 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Mon, 18 Apr 2022 08:33:33 -0700 Subject: [PATCH 01/82] Add parsing logic for translating from standard API strings (what we use in the CRV stats,[] This will be necessary for marking JDK/3p code (as either `@CheckReturnValue` or `@CanIgnoreReturnValue`). #checkreturnvalue PiperOrigin-RevId: 442548486 --- .../bugpatterns/checkreturnvalue/Api.java | 211 ++++++++++++++++++ .../bugpatterns/checkreturnvalue/ApiTest.java | 126 +++++++++++ 2 files changed, 337 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java 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..31a717e6c86 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -0,0 +1,211 @@ +/* + * 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.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +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 { + + // 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. */ + 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. + */ + abstract String methodName(); + + /** Returns the list of fully qualified parameter types for the given method/constructor. */ + 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()); + } + + private static final Splitter PARAM_SPLITTER = Splitter.on(','); + + /** + * 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 apiWithWhitespace) { + // TODO(kak): consider removing whitespace from the String as we step through the String + String api = whitespace().removeFrom(apiWithWhitespace); + + boolean isConstructor = false; + int hashIndex = -1; + int openParenIndex = -1; + int closeParenIndex = -1; + int lessThanIndex = -1; + int greaterThanIndex = -1; + for (int i = 0; i < api.length(); i++) { + char ch = api.charAt(i); + switch (ch) { + case '#': + check(hashIndex == -1, api, "it contains more than one '#'"); + hashIndex = i; + break; + case '(': + check(openParenIndex == -1, api, "it contains more than one '('"); + openParenIndex = i; + break; + case ')': + check(closeParenIndex == -1, api, "it contains more than one ')'"); + closeParenIndex = i; + break; + case '<': + check(lessThanIndex == -1, api, "it contains more than one '<'"); + lessThanIndex = i; + isConstructor = true; + break; + case '>': + check(greaterThanIndex == -1, api, "it contains more than one '>'"); + greaterThanIndex = i; + isConstructor = true; + break; + case ',': // for separating parameters + case '.': // for package names and fully qualified parameter names + break; + default: + check(isJavaIdentifierPart(ch), api, "'" + ch + "' is not a valid identifier"); + } + } + + // make sure we've seen a hash, open paren, and close paren + check(hashIndex != -1, api, "it must contain a '#'"); + check(openParenIndex != -1, api, "it must contain a '('"); + check(closeParenIndex == api.length() - 1, api, "it must end with ')'"); + + // make sure they came in the correct order: #() + check(hashIndex < openParenIndex, api, "'#' must come before '('"); + check(openParenIndex < closeParenIndex, api, "'(' must come before ')'"); + + if (isConstructor) { + // make sure that if we've seen a < or >, we also have seen the matching one + check(lessThanIndex != -1, api, "must contain both '<' and '>'"); + check(greaterThanIndex != -1, api, "must contain both '<' and '>'"); + + // make sure the < comes directly after the # + check(lessThanIndex == hashIndex + 1, api, "'<' must come directly after '#'"); + + // make sure that the < comes before the > + check(lessThanIndex < greaterThanIndex, api, "'<' must come before '>'"); + + // make sure that the > comes directly before the ( + check(greaterThanIndex == openParenIndex - 1, api, "'>' must come directly before '('"); + + // make sure the only thing between the < and > is exactly "init" + String constructorName = api.substring(lessThanIndex + 1, greaterThanIndex); + check(constructorName.equals("init"), api, "invalid method name: " + constructorName); + } + + String className = api.substring(0, hashIndex); + String methodName = api.substring(hashIndex + 1, openParenIndex); + String parameters = api.substring(openParenIndex + 1, closeParenIndex); + + ImmutableList paramList = + parameters.isEmpty() + ? ImmutableList.of() + : PARAM_SPLITTER.splitToStream(parameters).collect(toImmutableList()); + + // make sure the class name, method name, and parameter names are not empty + check(!className.isEmpty(), api, "the class name cannot be empty"); + check(!methodName.isEmpty(), api, "the method name cannot be empty"); + for (String parameter : paramList) { + check(!parameter.isEmpty(), api, "parameters cannot be empty"); + + check( + isJavaIdentifierStart(parameter.charAt(0)), + api, + "parameters must start with a valid character"); + } + // make sure the class name starts with a valid Java identifier character + check( + isJavaIdentifierStart(className.charAt(0)), + api, + "the class name must start with a valid character"); + + if (!isConstructor) { + // make sure the method name starts with a valid Java identifier character + check( + isJavaIdentifierStart(methodName.charAt(0)), + api, + "the method name must start with a valid character"); + } + + return new AutoValue_Api(className, methodName, paramList); + } + + private static void check(boolean condition, String api, String reason) { + checkArgument(condition, "Unable to parse '%s' because %s", api, reason); + } +} 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..41d9bab2685 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java @@ -0,0 +1,126 @@ +/* + * 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.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#<>()", + "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(string); + } + + @Test + public void parseApi_methodWithArray_b219754967() { + IllegalArgumentException thrown = + assertThrows( + "b/219754967 - cannot parse array signatures", + IllegalArgumentException.class, + () -> + Api.parse( + "com.google.inject.util.Modules.OverriddenModuleBuilder" + + "#with(com.google.inject.Module[])")); + assertThat(thrown).hasMessageThat().contains("'[' is not a valid identifier"); + } +} From f35b9df4107b77121e93c423b724341dd37aec55 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Mon, 18 Apr 2022 14:42:00 -0700 Subject: [PATCH 02/82] Remove duplicate `TypesWithUndefinedEquality#SPARSE_ARRAY` entry Type `androidx.collection.SparseArrayCompat` was listed twice. While there, sort the enum elements and type names. Fixes #3110 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/error-prone/pull/3110 from PicnicSupermarket:rossendrijver/object_undefined_equality c6979849a7c92ec10f019cf3358d06ff5ccc122c PiperOrigin-RevId: 442635974 --- .../TypesWithUndefinedEquality.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) 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; From 9b0d15a6c7cc4296995b3b80bda8937baf0a7dd6 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 18 Apr 2022 15:40:57 -0700 Subject: [PATCH 03/82] Auto-release the staging repository PiperOrigin-RevId: 442650283 --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index ecf68575fa5..f2161ec1eba 100644 --- a/pom.xml +++ b/pom.xml @@ -248,6 +248,7 @@ ossrh https://oss.sonatype.org/ + true From ae0c9281b412042281a40ac6dc1509172ccf88a5 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 18 Apr 2022 15:47:28 -0700 Subject: [PATCH 04/82] Migrate to `ASTHelpers.enclosingPackage` PiperOrigin-RevId: 442651828 --- .../main/java/com/google/errorprone/util/ASTHelpers.java | 7 ++++++- .../main/java/com/google/errorprone/util/Visibility.java | 3 ++- .../com/google/errorprone/bugpatterns/HidingField.java | 3 ++- .../google/errorprone/bugpatterns/MixedDescriptors.java | 3 ++- .../google/errorprone/bugpatterns/RedundantOverride.java | 3 ++- .../google/errorprone/bugpatterns/ReturnValueIgnored.java | 5 +++-- .../com/google/errorprone/bugpatterns/StaticImports.java | 3 ++- .../google/errorprone/bugpatterns/UnnecessaryLambda.java | 3 ++- .../bugpatterns/inject/dagger/RefersToDaggerCodegen.java | 4 +++- 9 files changed, 24 insertions(+), 10 deletions(-) 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..2b3e76ae4ab 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; 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/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/MixedDescriptors.java b/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java index 3f316aa378f..daec2207d1c 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; @@ -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/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/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/UnnecessaryLambda.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java index a8ae6598291..59249528047 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,7 +184,7 @@ 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 { 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) { From 11a65286da820da2b5e80927c1315e162d01aa32 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 19 Apr 2022 02:47:05 -0700 Subject: [PATCH 05/82] ImmutableChecker: for unqualified method invocations, bubble up the current path to work out what the effective type of the receiver is. PiperOrigin-RevId: 442756721 --- .../threadsafety/ImmutableChecker.java | 28 ++++++++- .../threadsafety/ImmutableCheckerTest.java | 60 +++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) 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..30256518638 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,14 @@ 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.getSymbol; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.isSubtype; import static java.lang.String.format; import static java.util.stream.Collectors.joining; @@ -60,6 +62,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; @@ -137,9 +140,8 @@ 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); + effectiveTypeOfThis(symbol, getCurrentPath(), state) + .ifPresent(t -> typesClosed.put(t, symbol)); } } return super.visitMethodInvocation(tree, null); @@ -203,6 +205,19 @@ private void handleIdentifier(Symbol symbol) { return NO_MATCH; } + /** + * 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 checkClosedLambdaVariable( VarSymbol closedVariable, LambdaExpressionTree tree, @@ -233,6 +248,13 @@ private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) { @Override public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { checkInvocation(tree, getSymbol(tree), ((JCMemberReference) tree).referentType, state); + if (!matchLambdas) { + return NO_MATCH; + } + TypeSymbol lambdaType = getType(tree).tsym; + if (!hasImmutableAnnotation(lambdaType, state)) { + return NO_MATCH; + } return NO_MATCH; } 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..6e5a7fa4453 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 @@ -2621,7 +2621,7 @@ public void lambda_canCallMethodOnImmutableClass() { } @Test - public void subclassesOfMutableType() { + public void checksEffectiveTypeOfReceiver() { compilationHelper .addSourceLines( "Test.java", @@ -2631,14 +2631,64 @@ 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<>();", + " test(map::get);", + " }", + "}") + .doTest(); + } } From 3d8946b62b7d30280fc7baa9584a88dca526ea70 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Tue, 19 Apr 2022 08:08:56 -0700 Subject: [PATCH 06/82] Add additional flag to the CheckReturnValue analyzer to change the default for un-annotated methods to enforce return value checking. PiperOrigin-RevId: 442814459 --- .../bugpatterns/CheckReturnValue.java | 76 ++++++++++++++----- .../bugpatterns/CheckReturnValueTest.java | 20 +++++ 2 files changed, 76 insertions(+), 20 deletions(-) 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..24abbe2acd8 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -22,7 +22,6 @@ 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; @@ -59,14 +58,18 @@ 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 final ImmutableMap ANNOTATIONS = + ImmutableMap.of( + CHECK_RETURN_VALUE, + CrvOpinion.SHOULD_BE_CRV, + CAN_IGNORE_RETURN_VALUE, + CrvOpinion.SHOULD_BE_CIRV); private static Stream findAnnotation(Symbol sym) { - return ANNOTATIONS.stream() - .filter(annotation -> hasDirectAnnotationWithSimpleName(sym, annotation)) + return ANNOTATIONS.entrySet().stream() + .filter(annoSpec -> hasDirectAnnotationWithSimpleName(sym, annoSpec.getKey())) .limit(1) - .map(annotation -> FoundAnnotation.create(annotation, scope(sym))); + .map(annotation -> FoundAnnotation.create(scope(sym), annotation.getValue())); } private static Optional firstAnnotation(MethodSymbol sym) { @@ -84,28 +87,60 @@ private static AnnotationScope scope(Symbol sym) { } static final String CHECK_ALL_CONSTRUCTORS = "CheckReturnValue:CheckAllConstructors"; + static final String CHECK_ALL_METHODS = "CheckReturnValue:CheckAllMethods"; private final boolean checkAllConstructors; + private final boolean checkAllMethods; public CheckReturnValue(ErrorProneFlags flags) { super(flags); this.checkAllConstructors = flags.getBoolean(CHECK_ALL_CONSTRUCTORS).orElse(false); + this.checkAllMethods = flags.getBoolean(CHECK_ALL_METHODS).orElse(false); } /** - * 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)); + Optional maybeMethod = methodToInspect(tree); + if (!maybeMethod.isPresent()) { + return false; + } + + return crvOpinionForMethod(maybeMethod.get()) + .map(CrvOpinion.SHOULD_BE_CRV::equals) + .orElse(false); }; } + private Optional crvOpinionForMethod(MethodSymbol sym) { + Optional opinionFromAnnotation = + firstAnnotation(sym).map(FoundAnnotation::checkReturnValueOpinion); + if (opinionFromAnnotation.isPresent()) { + return opinionFromAnnotation; + } + + // In the event there is no opinion from annotations, we use the checker's configuration to + // decide what the "default" for the universe is. + if (checkAllMethods || (checkAllConstructors && sym.isConstructor())) { + return Optional.of(CrvOpinion.SHOULD_BE_CRV); + } + // NB: You might consider this SHOULD_BE_CIRV (here, where the default is to not check any + // unannotated method, and no annotation exists) + // However, we also use this judgement in the "should be covered" part of the analysis, so we + // want to distinguish the states of "the world is CRV-by-default, but this method is annotated" + // from "the world is CIRV-by-default, and this method was unannotated". + return Optional.empty(); + } + + enum CrvOpinion { + SHOULD_BE_CRV, + SHOULD_BE_CIRV + } + private static Optional methodToInspect(ExpressionTree tree) { // If we're in the middle of calling an anonymous class, we want to actually look at the // corresponding constructor of the supertype (e.g.: if I extend a class with a @CIRV @@ -142,9 +177,7 @@ 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 methodSymbol(tree).flatMap(this::crvOpinionForMethod).isPresent(); } @Override @@ -213,7 +246,10 @@ && 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); + checkAllMethods + ? "Ignored return value of '%s', which wasn't annotated with @CanIgnoreReturnValue" + : "Ignored return value of '%s', which is annotated with @CheckReturnValue", + name); } @Override @@ -231,13 +267,13 @@ protected Description describeReturnValueIgnored(NewClassTree newClassTree, Visi @AutoValue abstract static class FoundAnnotation { - static FoundAnnotation create(String annotation, AnnotationScope scope) { - return new AutoValue_CheckReturnValue_FoundAnnotation(annotation, scope); + static FoundAnnotation create(AnnotationScope scope, CrvOpinion opinion) { + return new AutoValue_CheckReturnValue_FoundAnnotation(scope, opinion); } - abstract String annotation(); - abstract AnnotationScope scope(); + + abstract CrvOpinion checkReturnValueOpinion(); } enum AnnotationScope { 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..331dedb172f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -846,6 +846,22 @@ 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 usingElementInTestExpected() { compilationHelperLookingAtAllConstructors() @@ -1003,4 +1019,8 @@ private CompilationTestHelper compilationHelperLookingAtAllConstructors() { return compilationHelper.setArgs( "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); } + + private CompilationTestHelper compilationHelperLookingAtAllMethods() { + return compilationHelper.setArgs("-XepOpt:" + CheckReturnValue.CHECK_ALL_METHODS + "=true"); + } } From c3a263c6b8fb5affada6512c1c124db8d2324ed4 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 19 Apr 2022 09:16:07 -0700 Subject: [PATCH 07/82] Enable FuzzyEqualsShouldNotBeUsedInEqualsMethod. This was written years ago but (accidentally?) disabled. It's a great idea, though! PiperOrigin-RevId: 442828997 --- .../com/google/errorprone/scanner/BuiltInCheckerSuppliers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..6b7c9bebd75 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -644,6 +644,7 @@ public static ScannerSupplier errorChecks() { FromTemporalAccessor.class, FunctionalInterfaceMethodChanged.class, FuturesGetCheckedIllegalExceptionType.class, + FuzzyEqualsShouldNotBeUsedInEqualsMethod.class, GetClassOnAnnotation.class, GetClassOnClass.class, GuardedByChecker.class, @@ -1035,7 +1036,6 @@ public static ScannerSupplier errorChecks() { FloggerWithoutCause.class, ForEachIterable.class, FunctionalInterfaceClash.class, - FuzzyEqualsShouldNotBeUsedInEqualsMethod.class, HardCodedSdCardPath.class, ImmutableMemberCollection.class, ImmutableRefactoring.class, From f174a804a74b7ef413f946cd44977ad3059c2eee Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 19 Apr 2022 10:11:50 -0700 Subject: [PATCH 08/82] ImmutableChecker: handle method references. I think the only requirement for a method reference is that the receiver should be of an immutable type. PiperOrigin-RevId: 442843308 --- .../threadsafety/ImmutableChecker.java | 24 ++++++- .../threadsafety/ImmutableCheckerTest.java | 66 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) 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 30256518638..6bcd703fb90 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 @@ -21,10 +21,12 @@ 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.isSubtype; +import static com.google.errorprone.util.ASTHelpers.targetType; import static java.lang.String.format; import static java.util.stream.Collectors.joining; @@ -244,17 +246,35 @@ private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) { .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); if (!matchLambdas) { return NO_MATCH; } - TypeSymbol lambdaType = getType(tree).tsym; + TypeSymbol lambdaType = targetType(state).type().tsym; if (!hasImmutableAnnotation(lambdaType, state)) { return NO_MATCH; } + if (getSymbol(getReceiver(tree)) instanceof ClassSymbol) { + return NO_MATCH; + } + var receiverType = getReceiverType(tree); + ImmutableAnalysis analysis = createImmutableAnalysis(state); + 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 " + + lambdaType.getSimpleName() + + ", but " + + violation.message()) + .build(); + } return NO_MATCH; } 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 6e5a7fa4453..0e004f70923 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 @@ -2686,9 +2686,75 @@ public void methodReference_onMutableType() { " @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(); + } } From 5361233085b533a50bb22d6d7a1c51c853bfd6e2 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 20 Apr 2022 04:07:16 -0700 Subject: [PATCH 09/82] Turn down DivZero. It's aiming to replicate a javac warning, which I think we enable as an error anyway? PiperOrigin-RevId: 443055423 --- .../errorprone/bugpatterns/DivZero.java | 80 ----------------- .../scanner/BuiltInCheckerSuppliers.java | 2 - .../errorprone/bugpatterns/DivZeroTest.java | 42 --------- .../testdata/DivZeroNegativeCases.java | 25 ------ .../testdata/DivZeroPositiveCases.java | 87 ------------------- .../scanner/ScannerSupplierTest.java | 6 +- docs/bugpattern/DivZero.md | 1 - 7 files changed, 4 insertions(+), 239 deletions(-) delete mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/DivZero.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroNegativeCases.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroPositiveCases.java delete mode 100644 docs/bugpattern/DivZero.md 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 6b7c9bebd75..780593ddd83 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -101,7 +101,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; @@ -1016,7 +1015,6 @@ public static ScannerSupplier errorChecks() { DeduplicateConstants.class, DepAnn.class, DifferentNameButSame.class, - DivZero.class, EmptyIfStatement.class, EmptyTopLevelDeclaration.class, EqualsBrokenForNull.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java deleted file mode 100644 index a6482e5f232..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DivZeroTest.java +++ /dev/null @@ -1,42 +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 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) - */ -@RunWith(JUnit4.class) -public class DivZeroTest { - - private final CompilationTestHelper compilationHelper = - CompilationTestHelper.newInstance(DivZero.class, getClass()); - - @Test - public void testPositiveCase() { - compilationHelper.addSourceFile("DivZeroPositiveCases.java").doTest(); - } - - @Test - public void testNegativeCase() { - compilationHelper.addSourceFile("DivZeroNegativeCases.java").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/DivZeroNegativeCases.java deleted file mode 100644 index 6ae7a8b9de3..00000000000 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/DivZeroNegativeCases.java +++ /dev/null @@ -1,25 +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 DivZeroNegativeCases { - - void method(int a) { - double y = (double) a / 0.0; - } -} 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/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/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. From 919faf6786dd0500a23349ec7c84c10946897e76 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 20 Apr 2022 04:50:26 -0700 Subject: [PATCH 10/82] Remove ProtosAsKeyOfSetOrMap. This was added a fair while ago, and not enabled. PiperOrigin-RevId: 443061908 --- .../bugpatterns/ProtosAsKeyOfSetOrMap.java | 48 ----- .../scanner/BuiltInCheckerSuppliers.java | 2 - .../ProtosAsKeyOfSetOrMapTest.java | 167 ------------------ 3 files changed, 217 deletions(-) delete mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMap.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/ProtosAsKeyOfSetOrMapTest.java 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 780593ddd83..11070872e3c 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -281,7 +281,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; @@ -1068,7 +1067,6 @@ public static ScannerSupplier errorChecks() { PrimitiveArrayPassedToVarargsMethod.class, PrivateConstructorForNoninstantiableModule.class, PrivateConstructorForUtilityClass.class, - ProtosAsKeyOfSetOrMap.class, PublicApiNamedStreamShouldReturnStream.class, QualifierWithTypeUse.class, RedundantOverride.class, 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(); - } -} From 5a9395188c744b03683d79273041b72e93f25b66 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 20 Apr 2022 08:40:27 -0700 Subject: [PATCH 11/82] Have MultipleTopLevelClasses report findings on _every_ top level class, not just the first. Given JavaStyle shows on changed lines, you otherwise don't get findings when adding a second top level class to a file. PiperOrigin-RevId: 443104496 --- .../bugpatterns/MultipleTopLevelClasses.java | 22 ++++++++----------- .../MultipleTopLevelClassesTest.java | 16 +++++++++++--- 2 files changed, 22 insertions(+), 16 deletions(-) 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/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(); } From 614bd6f93ae6e8f884118fae90a8c6a272350f2a Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 20 Apr 2022 09:07:57 -0700 Subject: [PATCH 12/82] Internal change PiperOrigin-RevId: 443111024 --- docs/bugpattern/Immutable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From acfa835e88630640e68f18dcf7a8f58697d539d2 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 20 Apr 2022 10:51:45 -0700 Subject: [PATCH 13/82] ImmutableChecker: check @ImmutableTypeParams on lambdas and method references, too. PiperOrigin-RevId: 443137683 --- .../threadsafety/ImmutableChecker.java | 24 ++++++++++--- .../threadsafety/ImmutableCheckerTest.java | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) 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 6bcd703fb90..09998f4a385 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 @@ -122,6 +122,14 @@ public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState 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; } @@ -179,7 +187,6 @@ private void handleIdentifier(Symbol symbol) { } }.scan(state.getPath(), null); - ImmutableAnalysis analysis = createImmutableAnalysis(state); ImmutableSet typarams = immutableTypeParametersInScope(getSymbol(tree), state, analysis); variablesClosed.stream() @@ -253,15 +260,22 @@ public Description matchMemberReference(MemberReferenceTree tree, VisitorState s if (!matchLambdas) { return NO_MATCH; } - TypeSymbol lambdaType = targetType(state).type().tsym; - if (!hasImmutableAnnotation(lambdaType, 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); - ImmutableAnalysis analysis = createImmutableAnalysis(state); ImmutableSet typarams = immutableTypeParametersInScope(getSymbol(tree), state, analysis); var violation = @@ -270,7 +284,7 @@ public Description matchMemberReference(MemberReferenceTree tree, VisitorState s return buildDescription(tree) .setMessage( "This method reference implements @Immutable interface " - + lambdaType.getSimpleName() + + memberReferenceType.getSimpleName() + ", but " + violation.message()) .build(); 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 0e004f70923..04216acdef3 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 @@ -2757,4 +2757,38 @@ public void methodReference_toConstructor() { "}") .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(); + } } From 901d68132634955796a0ca408df440c9436a6262 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 21 Apr 2022 01:54:31 -0700 Subject: [PATCH 14/82] ImmutableChecker: closing around static fields is fine. (Whoops. I'm amazed this wasn't more common in the flume results; I only just spotted it.) PiperOrigin-RevId: 443311679 --- .../threadsafety/ImmutableChecker.java | 4 +++- .../threadsafety/ImmutableCheckerTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) 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 09998f4a385..38eb9b22fce 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 @@ -181,7 +181,9 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) { } private void handleIdentifier(Symbol symbol) { - if (symbol instanceof VarSymbol && !variablesOwnedByLambda.contains(symbol)) { + if (symbol instanceof VarSymbol + && !variablesOwnedByLambda.contains(symbol) + && !symbol.isStatic()) { variablesClosed.add((VarSymbol) symbol); } } 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 04216acdef3..0500e8a12c2 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 @@ -2578,6 +2578,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 From c030c53baca4e6c4ffd298e527e794ab48a2cc97 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 21 Apr 2022 05:24:13 -0700 Subject: [PATCH 15/82] Flag binary expressions involving & which results in a constant 0. This is often an erroneous mixed up bitwise operation. PiperOrigin-RevId: 443349569 --- .../ErroneousBitwiseExpression.java | 56 +++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../ErroneousBitwiseExpressionTest.java | 51 +++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpression.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/ErroneousBitwiseExpressionTest.java 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 11070872e3c..89b68fbf91b 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -120,6 +120,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; @@ -816,6 +817,7 @@ public static ScannerSupplier errorChecks() { EqualsIncompatibleType.class, EqualsUnsafeCast.class, EqualsUsingHashCode.class, + ErroneousBitwiseExpression.class, ErroneousThreadPoolConstructorChecker.class, EscapedEntity.class, ExtendingJUnitAssert.class, 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(); + } +} From bfd9b1d84238eba11b6baf3bfe366f2d9b733822 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 21 Apr 2022 08:14:11 -0700 Subject: [PATCH 16/82] Reuse ConstantExpressions in WrongOneof. The former was inspired by the latter, after all. PiperOrigin-RevId: 443381805 --- .../errorprone/bugpatterns/WrongOneof.java | 80 +++++-------------- 1 file changed, 19 insertions(+), 61 deletions(-) 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); From 330375e44d1996a945bfe4048133397e33c864eb Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 21 Apr 2022 09:25:22 -0700 Subject: [PATCH 17/82] Add an explanation for `WrongOneOf` PiperOrigin-RevId: 443398259 --- docs/bugpattern/WrongOneof.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/bugpattern/WrongOneof.md 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() +} +``` From f48903534c4897f80a7780c2564413307e6022fa Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 22 Apr 2022 03:11:59 -0700 Subject: [PATCH 18/82] Fix the workaround for SimpleCharStream. PiperOrigin-RevId: 443616431 --- .../google/errorprone/bugpatterns/MixedArrayDimensions.java | 4 +++- .../errorprone/bugpatterns/MixedArrayDimensionsTest.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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/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(); } From edd51fdab951da84704f093b338b1a8f72144310 Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 22 Apr 2022 07:50:25 -0700 Subject: [PATCH 19/82] Replace a labelled continue with a stream and remove some null checks. PiperOrigin-RevId: 443664780 --- .../errorprone/bugpatterns/FieldCanBeFinal.java | 16 +++++----------- .../bugpatterns/FieldCanBeFinalTest.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) 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/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 From 5bd42a2f33aa97dfe135c094122e13cb403e54da Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 22 Apr 2022 08:06:17 -0700 Subject: [PATCH 20/82] Enable EmptyTopLevelDeclaration. PiperOrigin-RevId: 443668189 --- .../bugpatterns/EmptyTopLevelDeclaration.java | 28 +++++++++---------- .../scanner/BuiltInCheckerSuppliers.java | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 89b68fbf91b..2a25444e2e1 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -628,6 +628,7 @@ public static ScannerSupplier errorChecks() { DurationGetTemporalUnit.class, DurationTemporalUnit.class, DurationToLongTimeUnit.class, + EmptyTopLevelDeclaration.class, EqualsHashCode.class, EqualsNaN.class, EqualsNull.class, @@ -1017,7 +1018,6 @@ public static ScannerSupplier errorChecks() { DepAnn.class, DifferentNameButSame.class, EmptyIfStatement.class, - EmptyTopLevelDeclaration.class, EqualsBrokenForNull.class, EqualsMissingNullable.class, ExpectedExceptionChecker.class, From 1e1ddd801b14f261f5dd1b24d9b7bcae188ef9a6 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Fri, 22 Apr 2022 12:31:24 -0700 Subject: [PATCH 21/82] Update example for SystemExitOutsideMain to more accurately describe the problem PiperOrigin-RevId: 443731509 --- docs/bugpattern/SystemExitOutsideMain.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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); From afda5fd3e7874307634dffeafe2142a13f49aa9b Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Fri, 22 Apr 2022 14:19:07 -0700 Subject: [PATCH 22/82] ErrorProne: Introduce rule: BanJNDI, which bans all methods in the JDK that can invoke JNDI. [] TGP (green): [] Files in violation: unknown commit PiperOrigin-RevId: 443756369 --- .../errorprone/bugpatterns/BanJNDI.java | 81 +++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../errorprone/bugpatterns/BanJNDITest.java | 52 ++++++ .../testdata/BanJNDINegativeCases.java | 35 ++++ .../testdata/BanJNDIPositiveCases.java | 149 +++++++++++++++ .../BanJNDIPositiveCases_expected.java | 169 ++++++++++++++++++ docs/bugpattern/BanJNDI.md | 64 +++++++ 7 files changed, 552 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/BanJNDI.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDIPositiveCases_expected.java create mode 100644 docs/bugpattern/BanJNDI.md 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 2a25444e2e1..0c1b0b9501e 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; @@ -601,6 +602,7 @@ public static ScannerSupplier errorChecks() { AutoValueConstructorOrderChecker.class, BadAnnotationImplementation.class, BadShiftAmount.class, + BanJNDI.class, BoxedPrimitiveEquality.class, BundleDeserializationCast.class, ChainingConstructorIgnoresParameter.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java b/core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java new file mode 100644 index 00000000000..e89b1bd4e93 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/BanJNDITest.java @@ -0,0 +1,52 @@ +/* + * 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 com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** {@link BanJNDI}Test */ +@RunWith(JUnit4.class) +public class BanJNDITest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(BanJNDI.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(BanJNDI.class, getClass()); + + @Test + public void testPositiveCase() { + compilationHelper.addSourceFile("BanJNDIPositiveCases.java").doTest(); + } + + @Test + public void testNegativeCase() { + 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/testdata/BanJNDINegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.java new file mode 100644 index 00000000000..fd5455c3c85 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/BanJNDINegativeCases.java @@ -0,0 +1,35 @@ +/* + * 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 javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +/** + * {@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/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. From f9d8b4f854ed1be38b4f242f306ca8aa1c717f86 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Tue, 26 Apr 2022 12:39:34 -0700 Subject: [PATCH 23/82] Fix explanation doc for ComparableType PiperOrigin-RevId: 444638005 --- docs/bugpattern/ComparableType.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { ... } } ``` From 4bda612de46afa5faadf0d1a914c6497d1295c61 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 26 Apr 2022 14:24:03 -0700 Subject: [PATCH 24/82] Don't send clrobot findings for MissingDefault Sometimes the right fix is to add an `AssertionError` instead, mailing automated CLs to add `default: // fall out` doesn't seem that valuable vs. letting people discover these when the code is modified. PiperOrigin-RevId: 444666796 --- .../com/google/errorprone/bugpatterns/MissingDefault.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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) { From 8132050af2f02800ea193254a60881056a5ba4aa Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 26 Apr 2022 16:07:46 -0700 Subject: [PATCH 25/82] Migrate from deprecated `JCommander` constructor to calling `parse()` explicitly. (This also lets us remove an unnecessary `var unused` from the callsite.) #checkreturnvalue PiperOrigin-RevId: 444693515 --- docgen/src/main/java/com/google/errorprone/DocGenTool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) { From 3f690243b0c7f1fa6284a9ef7ff440060d027579 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 26 Apr 2022 21:14:46 -0700 Subject: [PATCH 26/82] Prepare to make SystemExitOutsideMain an error PiperOrigin-RevId: 444750801 --- .../google/errorprone/bugpatterns/SystemExitOutsideMain.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"); From 9f8fbb6250f6f90f0a4d500821414f6d8ad1ec91 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 27 Apr 2022 06:35:24 -0700 Subject: [PATCH 27/82] Fix typo: getFieldByNumber -> findFieldByNumber PiperOrigin-RevId: 444842503 --- .../com/google/errorprone/bugpatterns/MixedDescriptors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 daec2207d1c..19a89f4b58d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MixedDescriptors.java @@ -51,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 { From becd4a2a5dc508a3b540e54e8d58736852499383 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Wed, 27 Apr 2022 08:31:11 -0700 Subject: [PATCH 28/82] Automatic code cleanup. PiperOrigin-RevId: 444866944 --- .../com/google/errorprone/bugpatterns/IgnoredPureGetter.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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..5cededb3d8d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/IgnoredPureGetter.java @@ -58,7 +58,6 @@ public final class IgnoredPureGetter extends AbstractReturnValueIgnored { state -> state.getTypeFromString("com.google.protobuf.MutableMessageLite")); private final boolean checkAllProtos; - private final boolean checkAutoBuilders; public IgnoredPureGetter() { this(ErrorProneFlags.empty()); @@ -67,7 +66,6 @@ public IgnoredPureGetter() { public IgnoredPureGetter(ErrorProneFlags flags) { super(flags); this.checkAllProtos = flags.getBoolean("IgnoredPureGetter:CheckAllProtos").orElse(true); - this.checkAutoBuilders = flags.getBoolean("IgnoredPureGetter:CheckAutoBuilders").orElse(true); } @Override @@ -126,8 +124,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); } From 2df37e8f9b2f10f55ff0ca115932eb9301c586ba Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Wed, 27 Apr 2022 09:10:16 -0700 Subject: [PATCH 29/82] typo fixes: One the other hand -> On the other hand want want -> want PiperOrigin-RevId: 444877119 --- docs/bugpattern/UnnecessaryDefaultInEnumSwitch.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From c0762c59a86a2b2763bd5525ff385fde8a089c0a Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Thu, 28 Apr 2022 10:21:03 -0700 Subject: [PATCH 30/82] Allow an external resource list with a number of API signatures to be loaded and used to declare a CanIgnoreReturnValue annotation to be present on a method. PiperOrigin-RevId: 445185970 --- .../bugpatterns/CheckReturnValue.java | 50 +++++--- .../ExternalCanIgnoreReturnValue.java | 109 ++++++++++++++++++ .../bugpatterns/CheckReturnValueTest.java | 40 +++++++ 3 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java 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 24abbe2acd8..ba8fd527906 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -28,6 +28,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; @@ -41,7 +42,6 @@ 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; @@ -65,15 +65,35 @@ public class CheckReturnValue extends AbstractReturnValueIgnored CAN_IGNORE_RETURN_VALUE, CrvOpinion.SHOULD_BE_CIRV); - private static Stream findAnnotation(Symbol sym) { + private static Optional controllingAnnotation( + MethodSymbol sym, VisitorState visitorState) { + // In priority order, we want a source-local annotation on the symbol, then the external API + // file, then the enclosing elements of sym. + return findAnnotation(sym) + .or( + () -> + asAnnotationFromConfig(sym, visitorState) + .or(() -> findAnnotationOnEnclosingSymbols(sym.owner))); + } + + private static Optional findAnnotation(Symbol sym) { return ANNOTATIONS.entrySet().stream() .filter(annoSpec -> hasDirectAnnotationWithSimpleName(sym, annoSpec.getKey())) - .limit(1) - .map(annotation -> FoundAnnotation.create(scope(sym), annotation.getValue())); + .map(annotation -> FoundAnnotation.create(scope(sym), annotation.getValue())) + .findFirst(); + } + + private static Optional findAnnotationOnEnclosingSymbols(Symbol sym) { + return ASTHelpers.enclosingElements(sym).flatMap(e -> findAnnotation(e).stream()).findFirst(); } - private static Optional firstAnnotation(MethodSymbol sym) { - return ASTHelpers.enclosingElements(sym).flatMap(CheckReturnValue::findAnnotation).findFirst(); + private static Optional asAnnotationFromConfig( + MethodSymbol sym, VisitorState visitorState) { + if (ExternalCanIgnoreReturnValue.externallyConfiguredCirvAnnotation(sym, visitorState)) { + return Optional.of( + FoundAnnotation.create(AnnotationScope.METHOD_EXTERNAL_ANNO, CrvOpinion.SHOULD_BE_CIRV)); + } + return Optional.empty(); } private static AnnotationScope scope(Symbol sym) { @@ -106,21 +126,20 @@ public CheckReturnValue(ErrorProneFlags flags) { public Matcher specializedMatcher() { return (tree, state) -> { Optional maybeMethod = methodToInspect(tree); - if (!maybeMethod.isPresent()) { + if (maybeMethod.isEmpty()) { return false; } - return crvOpinionForMethod(maybeMethod.get()) + return crvOpinionForMethod(maybeMethod.get(), state) .map(CrvOpinion.SHOULD_BE_CRV::equals) .orElse(false); }; } - private Optional crvOpinionForMethod(MethodSymbol sym) { - Optional opinionFromAnnotation = - firstAnnotation(sym).map(FoundAnnotation::checkReturnValueOpinion); - if (opinionFromAnnotation.isPresent()) { - return opinionFromAnnotation; + private Optional crvOpinionForMethod(MethodSymbol sym, VisitorState state) { + Optional annotationForSymbol = controllingAnnotation(sym, state); + if (annotationForSymbol.isPresent()) { + return annotationForSymbol.map(FoundAnnotation::checkReturnValueOpinion); } // In the event there is no opinion from annotations, we use the checker's configuration to @@ -177,13 +196,13 @@ private static Optional methodSymbol(ExpressionTree tree) { @Override public boolean isCovered(ExpressionTree tree, VisitorState state) { - return methodSymbol(tree).flatMap(this::crvOpinionForMethod).isPresent(); + return methodSymbol(tree).flatMap(sym -> crvOpinionForMethod(sym, state)).isPresent(); } @Override public ImmutableMap getMatchMetadata(ExpressionTree tree, VisitorState state) { return methodSymbol(tree) - .flatMap(CheckReturnValue::firstAnnotation) + .flatMap(sym -> controllingAnnotation(sym, state)) .map(found -> ImmutableMap.of("annotation_scope", found.scope())) .orElse(ImmutableMap.of()); } @@ -278,6 +297,7 @@ static FoundAnnotation create(AnnotationScope scope, CrvOpinion opinion) { enum AnnotationScope { METHOD, + METHOD_EXTERNAL_ANNO, CLASS, PACKAGE } 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..ce7bbaea462 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java @@ -0,0 +1,109 @@ +/* + * 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 java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Iterables; +import com.google.common.io.LineProcessor; +import com.google.common.io.MoreFiles; +import com.google.errorprone.VisitorState; +import com.google.errorprone.suppliers.Supplier; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Paths; +import java.util.Optional; + +/** External source of information about @CanIgnoreReturnValue-equivalent API's. */ +public class ExternalCanIgnoreReturnValue { + private ExternalCanIgnoreReturnValue() {} + + private static final String EXTERNAL_API_EXCLUSION_LIST = "CheckReturnValue:ApiExclusionList"; + + private static final Supplier> EXTERNAL_RESOURCE = + VisitorState.memoize( + state -> + state + .errorProneOptions() + .getFlags() + .get(EXTERNAL_API_EXCLUSION_LIST) + .map(ExternalCanIgnoreReturnValue::tryLoadingConfigFile)); + + public static boolean externallyConfiguredCirvAnnotation(MethodSymbol m, VisitorState s) { + return EXTERNAL_RESOURCE.get(s).map(protoList -> protoList.methodMatches(m, s)).orElse(false); + } + + private static FromConfig tryLoadingConfigFile(String filename) { + try { + ImmutableSetMultimap apis = + MoreFiles.asCharSource(Paths.get(filename), UTF_8) + .readLines( + new LineProcessor<>() { + private final ImmutableSetMultimap.Builder collectedApis = + ImmutableSetMultimap.builder(); + + @Override + public boolean processLine(String line) { + Api parsed = Api.parse(line); + collectedApis.put(parsed.className(), parsed); + return true; + } + + @Override + public ImmutableSetMultimap getResult() { + return collectedApis.build(); + } + }); + return new FromConfig(apis); + } catch (IOException e) { + throw new UncheckedIOException( + "Could not load external resource for CanIgnoreReturnValue", e); + } + } + + private static final class FromConfig { + // TODO(glorioso): Lots of different ways to think about this one here. + // * Do we make Api Comparable and use SortedSet? + // * Index by outer class? + // * Just Set? + private final ImmutableSetMultimap apis; + + private FromConfig(ImmutableSetMultimap apis) { + this.apis = apis; + } + + boolean methodMatches(MethodSymbol methodSymbol, VisitorState state) { + return apis.get(methodSymbol.enclClass().getQualifiedName().toString()).stream() + .anyMatch(api -> apiMatchesMethodSymbol(methodSymbol, api, state)); + } + + private static boolean apiMatchesMethodSymbol( + MethodSymbol methodSymbol, Api api, VisitorState state) { + if (!methodSymbol.getSimpleName().contentEquals(api.methodName())) { + return false; + } + + // Check for compatibility of these params. + return Iterables.elementsEqual( + api.parameterTypes(), + Iterables.transform( + methodSymbol.params(), p -> state.getTypes().erasure(p.type).toString())); + } + } +} 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 331dedb172f..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(); @@ -862,6 +873,22 @@ public void allMethods_withoutCIRVAnnotation() { .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() @@ -1023,4 +1050,17 @@ private CompilationTestHelper compilationHelperLookingAtAllConstructors() { 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); + } + } } From 418e8d07657e3291874fcc02fe9b2520c3a753b4 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Fri, 29 Apr 2022 13:46:26 -0700 Subject: [PATCH 31/82] Require all protobuf message calls to be used. #checkreturnvalue PiperOrigin-RevId: 445497002 --- .../bugpatterns/IgnoredPureGetter.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) 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 5cededb3d8d..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,20 +57,17 @@ public final class IgnoredPureGetter extends AbstractReturnValueIgnored { VisitorState.memoize( state -> state.getTypeFromString("com.google.protobuf.MutableMessageLite")); - private final boolean checkAllProtos; - public IgnoredPureGetter() { this(ErrorProneFlags.empty()); } public IgnoredPureGetter(ErrorProneFlags flags) { super(flags); - this.checkAllProtos = flags.getBoolean("IgnoredPureGetter:CheckAllProtos").orElse(true); } @Override protected Matcher specializedMatcher() { - return this::isPureGetter; + return IgnoredPureGetter::isPureGetter; } @Override @@ -103,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(); @@ -139,14 +134,7 @@ private Optional pureGetterKind(ExpressionTree tree, VisitorStat 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 From 575da416f32d7c6289888642b1cdb2bdf0e8f812 Mon Sep 17 00:00:00 2001 From: Chaoren Lin Date: Fri, 29 Apr 2022 15:39:36 -0700 Subject: [PATCH 32/82] Make TopLevelName in AL and ClassName in EP aliases of each other. They're not exactly the same, but it's still nice to have a way for tools to figure out the correspondence. Both aliases also still make sense on their own without taking into account the other check framework. PiperOrigin-RevId: 445522248 --- .../main/java/com/google/errorprone/bugpatterns/ClassName.java | 1 + 1 file changed, 1 insertion(+) 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, From 56dfc7912b6300bae8706b241c19054a582be79b Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Fri, 29 Apr 2022 16:28:32 -0700 Subject: [PATCH 33/82] Exclude UngroupedOverloads from STYLE category PiperOrigin-RevId: 445531199 --- .../com/google/errorprone/bugpatterns/UngroupedOverloads.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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; From 3c5cc816b77a035783f191995b785a3606c800e3 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Mon, 2 May 2022 12:51:11 -0700 Subject: [PATCH 34/82] Make ModifiedButNotUsed understand the put methods of proto map fields, and allow CheckReturnValue to identify all proto modifier methods as methods whose result can be ignored. PiperOrigin-RevId: 446006289 --- .../bugpatterns/CheckReturnValue.java | 17 +++++++++++++---- .../bugpatterns/ModifiedButNotUsed.java | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) 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 ba8fd527906..23dd94b0860 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -130,13 +130,14 @@ public Matcher specializedMatcher() { return false; } - return crvOpinionForMethod(maybeMethod.get(), state) + return crvOpinionForMethod(maybeMethod.get(), tree, state) .map(CrvOpinion.SHOULD_BE_CRV::equals) .orElse(false); }; } - private Optional crvOpinionForMethod(MethodSymbol sym, VisitorState state) { + private Optional crvOpinionForMethod( + MethodSymbol sym, ExpressionTree tree, VisitorState state) { Optional annotationForSymbol = controllingAnnotation(sym, state); if (annotationForSymbol.isPresent()) { return annotationForSymbol.map(FoundAnnotation::checkReturnValueOpinion); @@ -144,7 +145,15 @@ private Optional crvOpinionForMethod(MethodSymbol sym, VisitorState // In the event there is no opinion from annotations, we use the checker's configuration to // decide what the "default" for the universe is. - if (checkAllMethods || (checkAllConstructors && sym.isConstructor())) { + if (checkAllConstructors && sym.isConstructor()) { + return Optional.of(CrvOpinion.SHOULD_BE_CRV); + } + + if (checkAllMethods) { + // There are carveouts for methods we know to be ignorable by default. + if (ModifiedButNotUsed.FLUENT_SETTER.matches(tree, state)) { + return Optional.of(CrvOpinion.SHOULD_BE_CIRV); + } return Optional.of(CrvOpinion.SHOULD_BE_CRV); } // NB: You might consider this SHOULD_BE_CIRV (here, where the default is to not check any @@ -196,7 +205,7 @@ private static Optional methodSymbol(ExpressionTree tree) { @Override public boolean isCovered(ExpressionTree tree, VisitorState state) { - return methodSymbol(tree).flatMap(sym -> crvOpinionForMethod(sym, state)).isPresent(); + return methodSymbol(tree).flatMap(sym -> crvOpinionForMethod(sym, tree, state)).isPresent(); } @Override 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..8aba04b40bd 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|remove|set|put).+")), instanceMethod() .onDescendantOfAny( GUAVA_IMMUTABLES.stream().map(c -> c + ".Builder").collect(toImmutableSet())) From f546548a41de98d6e4da4e513ce7b791de55057d Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 3 May 2022 08:04:09 -0700 Subject: [PATCH 35/82] PUBLIC: Add a test that demonstrates the Api parses var-args correctly. PiperOrigin-RevId: 446195120 --- .../bugpatterns/checkreturnvalue/ApiTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 index 41d9bab2685..1f668441f95 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java @@ -123,4 +123,17 @@ public void parseApi_methodWithArray_b219754967() { + "#with(com.google.inject.Module[])")); assertThat(thrown).hasMessageThat().contains("'[' is not a valid identifier"); } + + @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); + } } From ec0d610d821717f94a64329eadc127886a015603 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Wed, 4 May 2022 01:54:39 -0700 Subject: [PATCH 36/82] Allow parentheses and concatenation of all compile time constants, not just strings. PiperOrigin-RevId: 446396350 --- .../CompileTimeConstantExpressionMatcher.java | 12 ++++------- ...pileTimeConstantExpressionMatcherTest.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) 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/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(); From 6b7d310f096c00eaa9e49663416e281ceff5153a Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 4 May 2022 08:36:17 -0700 Subject: [PATCH 37/82] Implement "conservative" mode for `EqualsMissingNullable` and `VoidMissingNullable`. In both cases, the conservative version of the check operates only on code that is both `@NullMarked` and _not_ `testonly`. The result is a check that we could theoretically enable as a compile error inside Google anytime, though I don't have immediate plans to do that. (Also: I took this opportunity to finally introduce a helper method for reading the value of the "Nullness:Conservative" flag.) PiperOrigin-RevId: 446465851 --- core/pom.xml | 6 ++ .../nullness/EqualsMissingNullable.java | 17 +++++ .../nullness/FieldMissingNullable.java | 3 +- .../bugpatterns/nullness/NullnessUtils.java | 26 +++++++ .../nullness/ReturnMissingNullable.java | 3 +- .../nullness/VoidMissingNullable.java | 52 ++++++++++++++ .../nullness/EqualsMissingNullableTest.java | 45 ++++++++++--- .../nullness/VoidMissingNullableTest.java | 67 ++++++++++++++----- pom.xml | 1 + 9 files changed, 192 insertions(+), 28 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 813e137579f..a968768423a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -355,6 +355,12 @@ ${flogger.version} test + + org.jspecify + jspecify + ${jspecify.version} + test + 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..0d0a57e15b6 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,11 +19,14 @@ 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.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; @@ -40,8 +43,18 @@ 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; } @@ -52,6 +65,10 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { return NO_MATCH; } + if (beingConservative && !isInNullMarkedScope(parameter, state)) { + return NO_MATCH; + } + SuggestedFix fix = fixByAddingNullableAnnotationToType(state, parameterTree); if (fix.isEmpty()) { 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..690a721d7d0 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,7 @@ 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.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; @@ -57,7 +58,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 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..15f92825884 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; @@ -84,6 +85,31 @@ private NullnessUtils() {} private static final Matcher OPTIONAL_OR_ELSE = instanceMethod().onDescendantOf("java.util.Optional").named("orElse"); + 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. 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..4dabfe7c081 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 @@ -24,6 +24,7 @@ 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.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; @@ -185,7 +186,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 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/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java index 76385ab580a..78457c41f14 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;", @@ -94,7 +98,7 @@ public void testNegativeAlreadyAnnotated() { @Test public void testNegativeNotObjectEquals() { - helper + aggressiveHelper .addSourceLines( "Foo.java", "abstract class Foo {", @@ -102,4 +106,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/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/pom.xml b/pom.xml index f2161ec1eba..f6e3708749b 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ 1.6.8 3.19.2 1.43.2 + 0.2.0 From 0c497a19d67cf4c58ec7db390b604c7307ba7b55 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 5 May 2022 11:43:57 -0700 Subject: [PATCH 38/82] Guava Predicate -> Java8 Predicate. PiperOrigin-RevId: 446775706 --- .../NullnessPropagationTransfer.java | 9 ++-- .../BadAnnotationImplementation.java | 20 +++------ ...teConstructorForNoninstantiableModule.java | 44 ++++++------------- .../threadsafety/HeldLockAnalyzer.java | 8 ++-- .../time/PreferJavaTimeOverload.java | 4 +- 5 files changed, 30 insertions(+), 55 deletions(-) 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/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/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/threadsafety/HeldLockAnalyzer.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzer.java index 52d98bc3cdf..5e37c78ea2d 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; /** @@ -144,7 +144,7 @@ private LockScanner( @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 @@ -234,12 +234,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) { 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)) { From aa19b492f1d2f68626bafb56768b885ca752f731 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Thu, 5 May 2022 13:26:03 -0700 Subject: [PATCH 39/82] Allow the CheckReturnValue Api Parser to handle signatures with array types. PiperOrigin-RevId: 446801757 --- .../bugpatterns/checkreturnvalue/Api.java | 26 ++++++++++++++++++ .../bugpatterns/checkreturnvalue/ApiTest.java | 27 ++++++++++++------- 2 files changed, 43 insertions(+), 10 deletions(-) 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 index 31a717e6c86..489ca29c58a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -134,6 +134,8 @@ static Api parse(String apiWithWhitespace) { break; case ',': // for separating parameters case '.': // for package names and fully qualified parameter names + case '[': // for array signature types + case ']': // for array signature types break; default: check(isJavaIdentifierPart(ch), api, "'" + ch + "' is not a valid identifier"); @@ -187,6 +189,30 @@ static Api parse(String apiWithWhitespace) { 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 i = 1; i < parameter.length(); i++) { + char c = parameter.charAt(i); + 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"); } // make sure the class name starts with a valid Java identifier character check( 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 index 1f668441f95..d8660ff028c 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java @@ -42,6 +42,12 @@ public final class ApiTest { "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()", @@ -112,16 +118,17 @@ public void parseApi_methodWithParamsAndSpaces() { } @Test - public void parseApi_methodWithArray_b219754967() { - IllegalArgumentException thrown = - assertThrows( - "b/219754967 - cannot parse array signatures", - IllegalArgumentException.class, - () -> - Api.parse( - "com.google.inject.util.Modules.OverriddenModuleBuilder" - + "#with(com.google.inject.Module[])")); - assertThat(thrown).hasMessageThat().contains("'[' is not a valid identifier"); + 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 From a7734bcf5ed476c3cd3bd2b49db6b90bfe2c36dd Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 6 May 2022 03:41:03 -0700 Subject: [PATCH 40/82] Fulfil a TODO to use ifPresent. PiperOrigin-RevId: 446945879 --- .../bugpatterns/threadsafety/HeldLockAnalyzer.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) 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 5e37c78ea2d..5db198ab4d6 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 @@ -387,17 +387,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); } } } From 67fe479981b411ce91a7eb66579ad3c401bad4d9 Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 6 May 2022 05:40:00 -0700 Subject: [PATCH 41/82] Remove {Lock,Unlock}MethodChecker. These have never been enabled, and the annotations they check are super rare. UnlockMethod seems to be checked by GuardedBy anyway. PiperOrigin-RevId: 446965148 --- .../annotations/concurrent/LockMethod.java | 3 + .../annotations/concurrent/UnlockMethod.java | 3 + .../AbstractLockMethodChecker.java | 115 ------------ .../threadsafety/LockMethodChecker.java | 66 ------- .../threadsafety/UnlockMethodChecker.java | 66 ------- .../scanner/BuiltInCheckerSuppliers.java | 4 - .../threadsafety/LockMethodCheckerTest.java | 177 ------------------ .../threadsafety/UnlockMethodCheckerTest.java | 176 ----------------- docs/bugpattern/LockMethodChecker.md | 2 - docs/bugpattern/UnlockMethod.md | 3 - 10 files changed, 6 insertions(+), 609 deletions(-) delete mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/AbstractLockMethodChecker.java delete mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodChecker.java delete mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodChecker.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/LockMethodCheckerTest.java delete mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/UnlockMethodCheckerTest.java delete mode 100644 docs/bugpattern/LockMethodChecker.md delete mode 100644 docs/bugpattern/UnlockMethod.md 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/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/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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 0c1b0b9501e..d35b4ddd848 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -500,11 +500,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; @@ -1051,7 +1049,6 @@ public static ScannerSupplier errorChecks() { Java7ApiChecker.class, Java8ApiChecker.class, LambdaFunctionalInterface.class, - LockMethodChecker.class, LongLiteralLowerCaseSuffix.class, MemberName.class, MethodCanBeStatic.class, @@ -1101,7 +1098,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/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/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/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. From ba95fe72d7a30aa3bcdfb348216780b4f89a37c5 Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Fri, 6 May 2022 21:09:25 -0700 Subject: [PATCH 42/82] Create an evaluator for deciding if use of a method's result is expected (aka CRV) or optional (aka CIRV). The idea here (for now) is to match the behavior of the `CheckReturnValue` checker (i.e. looking at the method, then enclosing classes, etc.) while improving on it in a number of ways: - Make it easy to plug in new rules determining a result-use policy for methods. - Make it easy to see what rules apply and how they're prioritized. - Add a "global" scope that can be used for implementing defaults. - Allow us to check declarations for conflicts between rules in general, not just to check for something being annotated with both CRV and CIRV. This should allow us to, for example, prevent an `@AutoValue` getter method from being annotated with CIRV. - Make it far easier for us to get good data. Every rule can create an `Evaluation` with data that allows us to see exactly why any given method had a certain policy selected for it: the ID of the rule that selected the policy, the scope at which the selection was made, and the symbol that was evaluated to make that selection (for example the class that an annotation was found on). See unknown commit for an example of how easy extending this with new rules should be. PiperOrigin-RevId: 447130426 --- .../bugpatterns/CheckReturnValue.java | 164 ++++----------- .../ExternalCanIgnoreReturnValue.java | 21 +- .../checkreturnvalue/ProtoRules.java | 70 +++++++ .../checkreturnvalue/ResultUsePolicy.java | 30 +++ .../ResultUsePolicyEvaluator.java | 132 ++++++++++++ .../checkreturnvalue/ResultUseRule.java | 191 ++++++++++++++++++ .../bugpatterns/checkreturnvalue/Rules.java | 101 +++++++++ 7 files changed, 586 insertions(+), 123 deletions(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicy.java create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUsePolicyEvaluator.java create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ResultUseRule.java create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Rules.java 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 23dd94b0860..c687ab027b3 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -17,10 +17,15 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.protoBuilders; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.EXPECTED; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL; +import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.globalDefault; +import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.mapAnnotationSimpleName; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName; -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableMap; import com.google.errorprone.BugPattern; import com.google.errorprone.ErrorProneFlags; @@ -28,7 +33,8 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; -import com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; @@ -39,7 +45,6 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.util.Optional; import javax.lang.model.element.ElementKind; @@ -58,64 +63,29 @@ public class CheckReturnValue extends AbstractReturnValueIgnored private static final String CHECK_RETURN_VALUE = "CheckReturnValue"; private static final String CAN_IGNORE_RETURN_VALUE = "CanIgnoreReturnValue"; - private static final ImmutableMap ANNOTATIONS = - ImmutableMap.of( - CHECK_RETURN_VALUE, - CrvOpinion.SHOULD_BE_CRV, - CAN_IGNORE_RETURN_VALUE, - CrvOpinion.SHOULD_BE_CIRV); - - private static Optional controllingAnnotation( - MethodSymbol sym, VisitorState visitorState) { - // In priority order, we want a source-local annotation on the symbol, then the external API - // file, then the enclosing elements of sym. - return findAnnotation(sym) - .or( - () -> - asAnnotationFromConfig(sym, visitorState) - .or(() -> findAnnotationOnEnclosingSymbols(sym.owner))); - } - - private static Optional findAnnotation(Symbol sym) { - return ANNOTATIONS.entrySet().stream() - .filter(annoSpec -> hasDirectAnnotationWithSimpleName(sym, annoSpec.getKey())) - .map(annotation -> FoundAnnotation.create(scope(sym), annotation.getValue())) - .findFirst(); - } - - private static Optional findAnnotationOnEnclosingSymbols(Symbol sym) { - return ASTHelpers.enclosingElements(sym).flatMap(e -> findAnnotation(e).stream()).findFirst(); - } - - private static Optional asAnnotationFromConfig( - MethodSymbol sym, VisitorState visitorState) { - if (ExternalCanIgnoreReturnValue.externallyConfiguredCirvAnnotation(sym, visitorState)) { - return Optional.of( - FoundAnnotation.create(AnnotationScope.METHOD_EXTERNAL_ANNO, CrvOpinion.SHOULD_BE_CIRV)); - } - return Optional.empty(); - } - - private static AnnotationScope scope(Symbol sym) { - if (sym instanceof MethodSymbol) { - return AnnotationScope.METHOD; - } else if (sym instanceof ClassSymbol) { - return AnnotationScope.CLASS; - } else { - return AnnotationScope.PACKAGE; - } - } - static final String CHECK_ALL_CONSTRUCTORS = "CheckReturnValue:CheckAllConstructors"; static final String CHECK_ALL_METHODS = "CheckReturnValue:CheckAllMethods"; - private final boolean checkAllConstructors; - private final boolean checkAllMethods; + private final Optional 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.checkAllMethods = flags.getBoolean(CHECK_ALL_METHODS).orElse(false); + this.constructorPolicy = defaultPolicy(flags, CHECK_ALL_CONSTRUCTORS); + this.methodPolicy = defaultPolicy(flags, CHECK_ALL_METHODS); + + this.evaluator = + ResultUsePolicyEvaluator.create( + mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), + mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), + protoBuilders(), + externalIgnoreList(), + globalDefault(methodPolicy, constructorPolicy)); + } + + private static Optional defaultPolicy(ErrorProneFlags flags, String flag) { + return flags.getBoolean(flag).map(check -> check ? EXPECTED : OPTIONAL); } /** @@ -124,49 +94,11 @@ public CheckReturnValue(ErrorProneFlags flags) { */ @Override public Matcher specializedMatcher() { - return (tree, state) -> { - Optional maybeMethod = methodToInspect(tree); - if (maybeMethod.isEmpty()) { - return false; - } - - return crvOpinionForMethod(maybeMethod.get(), tree, state) - .map(CrvOpinion.SHOULD_BE_CRV::equals) - .orElse(false); - }; - } - - private Optional crvOpinionForMethod( - MethodSymbol sym, ExpressionTree tree, VisitorState state) { - Optional annotationForSymbol = controllingAnnotation(sym, state); - if (annotationForSymbol.isPresent()) { - return annotationForSymbol.map(FoundAnnotation::checkReturnValueOpinion); - } - - // In the event there is no opinion from annotations, we use the checker's configuration to - // decide what the "default" for the universe is. - if (checkAllConstructors && sym.isConstructor()) { - return Optional.of(CrvOpinion.SHOULD_BE_CRV); - } - - if (checkAllMethods) { - // There are carveouts for methods we know to be ignorable by default. - if (ModifiedButNotUsed.FLUENT_SETTER.matches(tree, state)) { - return Optional.of(CrvOpinion.SHOULD_BE_CIRV); - } - return Optional.of(CrvOpinion.SHOULD_BE_CRV); - } - // NB: You might consider this SHOULD_BE_CIRV (here, where the default is to not check any - // unannotated method, and no annotation exists) - // However, we also use this judgement in the "should be covered" part of the analysis, so we - // want to distinguish the states of "the world is CRV-by-default, but this method is annotated" - // from "the world is CIRV-by-default, and this method was unannotated". - return Optional.empty(); - } - - enum CrvOpinion { - SHOULD_BE_CRV, - SHOULD_BE_CIRV + return (tree, state) -> + methodToInspect(tree) + .map(method -> evaluator.evaluate(method, state)) + .orElse(OPTIONAL) + .equals(EXPECTED); } private static Optional methodToInspect(ExpressionTree tree) { @@ -205,14 +137,18 @@ private static Optional methodSymbol(ExpressionTree tree) { @Override public boolean isCovered(ExpressionTree tree, VisitorState state) { - return methodSymbol(tree).flatMap(sym -> crvOpinionForMethod(sym, tree, state)).isPresent(); + return methodToInspect(tree).stream() + .flatMap(method -> evaluator.evaluations(method, state)) + .findFirst() + .isPresent(); } @Override public ImmutableMap getMatchMetadata(ExpressionTree tree, VisitorState state) { - return methodSymbol(tree) - .flatMap(sym -> controllingAnnotation(sym, state)) - .map(found -> ImmutableMap.of("annotation_scope", found.scope())) + return methodToInspect(tree).stream() + .flatMap(method -> evaluator.evaluations(method, state)) + .findFirst() + .map(evaluation -> ImmutableMap.of("annotation_scope", evaluation.scope())) .orElse(ImmutableMap.of()); } @@ -234,6 +170,8 @@ public Description matchMethod(MethodTree tree, VisitorState state) { boolean checkReturn = hasDirectAnnotationWithSimpleName(method, CHECK_RETURN_VALUE); boolean canIgnore = hasDirectAnnotationWithSimpleName(method, CAN_IGNORE_RETURN_VALUE); + // TODO(cgdecker): We can check this with evaluator.checkForConflicts now, though I want to + // think more about how we build and format error messages in that. if (checkReturn && canIgnore) { return buildDescription(tree).setMessage(String.format(BOTH_ERROR, "method")).build(); } @@ -274,7 +212,7 @@ && hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), CAN_IGNORE_RETU @Override protected String getMessage(Name name) { return String.format( - checkAllMethods + methodPolicy.orElse(OPTIONAL).equals(EXPECTED) ? "Ignored return value of '%s', which wasn't annotated with @CanIgnoreReturnValue" : "Ignored return value of '%s', which is annotated with @CheckReturnValue", name); @@ -282,7 +220,7 @@ protected String getMessage(Name name) { @Override protected Description describeReturnValueIgnored(NewClassTree newClassTree, VisitorState state) { - return checkAllConstructors + return constructorPolicy.orElse(OPTIONAL).equals(EXPECTED) ? buildDescription(newClassTree) .setMessage( String.format( @@ -292,22 +230,4 @@ protected Description describeReturnValueIgnored(NewClassTree newClassTree, Visi .build() : super.describeReturnValueIgnored(newClassTree, state); } - - @AutoValue - abstract static class FoundAnnotation { - static FoundAnnotation create(AnnotationScope scope, CrvOpinion opinion) { - return new AutoValue_CheckReturnValue_FoundAnnotation(scope, opinion); - } - - abstract AnnotationScope scope(); - - abstract CrvOpinion checkReturnValueOpinion(); - } - - enum AnnotationScope { - METHOD, - METHOD_EXTERNAL_ANNO, - CLASS, - PACKAGE - } } 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 index ce7bbaea462..72e8ea2bcf9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java @@ -23,6 +23,7 @@ import com.google.common.io.LineProcessor; import com.google.common.io.MoreFiles; import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule; import com.google.errorprone.suppliers.Supplier; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.io.IOException; @@ -31,7 +32,13 @@ import java.util.Optional; /** External source of information about @CanIgnoreReturnValue-equivalent API's. */ -public class ExternalCanIgnoreReturnValue { +public final class ExternalCanIgnoreReturnValue extends MethodRule { + + /** Returns a rule using an external list of APIs to ignore. */ + public static ResultUseRule externalIgnoreList() { + return new ExternalCanIgnoreReturnValue(); + } + private ExternalCanIgnoreReturnValue() {} private static final String EXTERNAL_API_EXCLUSION_LIST = "CheckReturnValue:ApiExclusionList"; @@ -45,6 +52,18 @@ private ExternalCanIgnoreReturnValue() {} .get(EXTERNAL_API_EXCLUSION_LIST) .map(ExternalCanIgnoreReturnValue::tryLoadingConfigFile)); + @Override + public String id() { + return "EXTERNAL_API_EXCLUSION_LIST"; + } + + @Override + public Optional evaluateMethod(MethodSymbol method, VisitorState state) { + return externallyConfiguredCirvAnnotation(method, state) + ? Optional.of(ResultUsePolicy.OPTIONAL) + : Optional.empty(); + } + public static boolean externallyConfiguredCirvAnnotation(MethodSymbol m, VisitorState s) { return EXTERNAL_RESOURCE.get(s).map(protoList -> protoList.methodMatches(m, s)).orElse(false); } 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..cbccff5c29d --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.errorprone.bugpatterns.checkreturnvalue; + +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule; +import com.google.errorprone.suppliers.Supplier; +import com.google.errorprone.util.ASTHelpers; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; +import java.util.Optional; +import java.util.regex.Pattern; + +/** Rules for methods on proto messages and builders. */ +public final class ProtoRules { + + private ProtoRules() {} + + /** + * Returns a rule that handles proto builders, making their fluent setter methods' results + * ignorable. + */ + public static ResultUseRule protoBuilders() { + return new ProtoBuilder(); + } + + private static final Supplier MESSAGE_LITE_BUILDER = + supplier("com.google.protobuf.MessageLite.Builder"); + + // TODO(cgdecker): Move proto rules from IgnoredPureGetter and ReturnValueIgnored here + + /** Rules for methods on proto builders. */ + private static final class ProtoBuilder extends MethodRule { + private static final Pattern SETTERS = Pattern.compile("(add|clear|remove|set|put).+"); + + @Override + public String id() { + return "PROTO_BUILDER"; + } + + @Override + public Optional evaluateMethod(MethodSymbol method, VisitorState state) { + Type ownerType = method.owner.type; + if (ASTHelpers.isSubtype(ownerType, MESSAGE_LITE_BUILDER.get(state), state)) { + if (SETTERS.matcher(method.name).matches()) { + return Optional.of(ResultUsePolicy.OPTIONAL); + } + } + return Optional.empty(); + } + } + + private static Supplier 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; + } + } +} From 9a5cb2d0a3d7d68dfed6a049147d2efafa0f3252 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Sun, 8 May 2022 14:15:41 -0700 Subject: [PATCH 43/82] Update ci.yml Use released 18, add 19-ea --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 }} From 4069d9194dd97e6d48e32e6a975003764dd2cd48 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 9 May 2022 11:25:49 -0700 Subject: [PATCH 44/82] Add a check for thread interruption policies PiperOrigin-RevId: 447520694 --- .../errorprone/bugpatterns/Interruption.java | 145 ++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../bugpatterns/InterruptionTest.java | 143 +++++++++++++++++ docs/bugpattern/Interruption.md | 0 4 files changed, 290 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java create mode 100644 docs/bugpattern/Interruption.md 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..32f521ba837 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java @@ -0,0 +1,145 @@ +/* + * 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() + .onDescendantOf("java.util.concurrent.Future") + .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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index d35b4ddd848..bd4c2fb69c8 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -169,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; @@ -1045,6 +1046,7 @@ public static ScannerSupplier errorChecks() { InsecureCipherMode.class, InterfaceWithOnlyStatics.class, InterruptedExceptionSwallowed.class, + Interruption.class, IterablePathParameter.class, Java7ApiChecker.class, Java8ApiChecker.class, 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..82a4c6b2111 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java @@ -0,0 +1,143 @@ +/* + * 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 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/docs/bugpattern/Interruption.md b/docs/bugpattern/Interruption.md new file mode 100644 index 00000000000..e69de29bb2d From f8261fa8b82504e248c5cdbcf187513bccaa678e Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Mon, 9 May 2022 12:37:39 -0700 Subject: [PATCH 45/82] Treat getters-of-submessages on Proto Builders as ignorable, since the act of getting those submessages produces an observable modification. PiperOrigin-RevId: 447538024 --- .../checkreturnvalue/ProtoRules.java | 20 ++++++++++++++++--- core/src/test/proto/proto_test.proto | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) 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 index cbccff5c29d..40b75e31a76 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java @@ -54,14 +54,28 @@ public String id() { @Override public Optional evaluateMethod(MethodSymbol method, VisitorState state) { - Type ownerType = method.owner.type; - if (ASTHelpers.isSubtype(ownerType, MESSAGE_LITE_BUILDER.get(state), state)) { - if (SETTERS.matcher(method.name).matches()) { + if (isProtoBuilderType(state, method.owner.type)) { + String methodName = method.name.toString(); + if (SETTERS.matcher(methodName).matches()) { + return Optional.of(ResultUsePolicy.OPTIONAL); + } + if (isGetterOfSubmessageBuilder(methodName) + && isProtoBuilderType(state, method.getReturnType())) { return Optional.of(ResultUsePolicy.OPTIONAL); } } return Optional.empty(); } + + private static boolean isProtoBuilderType(VisitorState state, Type ownerType) { + return ASTHelpers.isSubtype(ownerType, MESSAGE_LITE_BUILDER.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? + return name.startsWith("get") && name.endsWith("Builder") && !name.endsWith("OrBuilder"); + } } private static Supplier supplier(String name) { 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; } From 6d35842755a4c0554a551fbf3c748bb6e3680541 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 10 May 2022 09:54:43 -0700 Subject: [PATCH 46/82] Make `protoBuilder.merge*()` and `protoBuilder.clear*()` implicitly `@CanIgnoreReturnValue`. #checkreturnvalue PiperOrigin-RevId: 447758713 --- .../errorprone/bugpatterns/checkreturnvalue/ProtoRules.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 40b75e31a76..6e240250cc6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java @@ -45,7 +45,8 @@ public static ResultUseRule protoBuilders() { /** Rules for methods on proto builders. */ private static final class ProtoBuilder extends MethodRule { - private static final Pattern SETTERS = Pattern.compile("(add|clear|remove|set|put).+"); + private static final Pattern RETURNS_THIS = + Pattern.compile("(add|clear|merge|remove|set|put).*"); @Override public String id() { @@ -56,7 +57,7 @@ public String id() { public Optional evaluateMethod(MethodSymbol method, VisitorState state) { if (isProtoBuilderType(state, method.owner.type)) { String methodName = method.name.toString(); - if (SETTERS.matcher(methodName).matches()) { + if (RETURNS_THIS.matcher(methodName).matches()) { return Optional.of(ResultUsePolicy.OPTIONAL); } if (isGetterOfSubmessageBuilder(methodName) From a29239cedca70310499eba029fabbf39dcab32b5 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 10 May 2022 10:07:15 -0700 Subject: [PATCH 47/82] Hardcode matchLambdas. PiperOrigin-RevId: 447762247 --- .../bugpatterns/threadsafety/ImmutableChecker.java | 8 -------- 1 file changed, 8 deletions(-) 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 38eb9b22fce..0666bc07440 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 @@ -100,7 +100,6 @@ public class ImmutableChecker extends BugChecker private final WellKnownMutability wellKnownMutability; private final ImmutableSet immutableAnnotations; - private final boolean matchLambdas; ImmutableChecker(ImmutableSet immutableAnnotations) { this(ErrorProneFlags.empty(), immutableAnnotations); @@ -113,14 +112,10 @@ 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); } @Override public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) { - if (!matchLambdas) { - return NO_MATCH; - } TypeSymbol lambdaType = getType(tree).tsym; ImmutableAnalysis analysis = createImmutableAnalysis(state); Violation info = @@ -259,9 +254,6 @@ private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) { public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { // check instantiations of `@ImmutableTypeParameter`s in method references checkInvocation(tree, getSymbol(tree), ((JCMemberReference) tree).referentType, state); - if (!matchLambdas) { - return NO_MATCH; - } ImmutableAnalysis analysis = createImmutableAnalysis(state); TypeSymbol memberReferenceType = targetType(state).type().tsym; Violation info = From b10b761c79f4bf7e7b2ccc32039737cee95052cc Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 10 May 2022 11:15:01 -0700 Subject: [PATCH 48/82] Recognize that most calls to `foo(null)` prove that `foo`'s argument should be `@Nullable`. Also, demote the existing `ParameterMissingNullable` logic, which is more heuristic-based, to run only in aggressive mode. PiperOrigin-RevId: 447780998 --- .../nullness/EqualsMissingNullable.java | 5 +- .../nullness/FieldMissingNullable.java | 5 +- .../bugpatterns/nullness/NullnessUtils.java | 17 ++ .../nullness/ParameterMissingNullable.java | 133 ++++++++++++- .../nullness/ReturnMissingNullable.java | 8 +- .../ParameterMissingNullableTest.java | 187 +++++++++++++++--- 6 files changed, 316 insertions(+), 39 deletions(-) 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 0d0a57e15b6..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,6 +19,7 @@ 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; @@ -30,8 +31,6 @@ 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; @@ -61,7 +60,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { VariableTree parameterTree = getOnlyElement(methodTree.getParameters()); VarSymbol parameter = getSymbol(parameterTree); - if (NullnessAnnotations.fromAnnotationsOn(parameter).orElse(null) == Nullness.NULLABLE) { + if (isAlreadyAnnotatedNullable(parameter)) { 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 690a721d7d0..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,7 @@ 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; @@ -36,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; @@ -131,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 15f92825884..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 @@ -38,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; @@ -85,6 +87,17 @@ 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); } @@ -214,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 4dabfe7c081..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,6 +23,7 @@ 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; @@ -47,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; @@ -269,8 +268,7 @@ void doVisitMethod(MethodTree tree) { * codebase). */ - if (NullnessAnnotations.fromAnnotationsOn(possibleOverride).orElse(null) - == Nullness.NULLABLE) { + if (isAlreadyAnnotatedNullable(possibleOverride)) { return; } @@ -347,7 +345,7 @@ && methodCanBeOverridden(method)) { return; } - if (NullnessAnnotations.fromAnnotationsOn(method).orElse(null) == Nullness.NULLABLE) { + if (isAlreadyAnnotatedNullable(method)) { return; } 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(); + } } From c805dac7ee2f5b3a12e10101d7193c8b0424a583 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 10 May 2022 12:51:49 -0700 Subject: [PATCH 49/82] Allow return values of mutable proto message setters to be ignored. #checkreturnvalue PiperOrigin-RevId: 447804671 --- .../bugpatterns/CheckReturnValue.java | 2 + .../checkreturnvalue/ProtoRules.java | 39 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) 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 c687ab027b3..b5d1c8436a4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -18,6 +18,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.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; @@ -80,6 +81,7 @@ public CheckReturnValue(ErrorProneFlags flags) { mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), protoBuilders(), + mutableProtos(), externalIgnoreList(), globalDefault(methodPolicy, constructorPolicy)); } 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 index 6e240250cc6..ae3c334d82b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ProtoRules.java @@ -16,10 +16,12 @@ 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.google.errorprone.util.ASTHelpers; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Type; import java.util.Optional; @@ -35,46 +37,61 @@ private ProtoRules() {} * ignorable. */ public static ResultUseRule protoBuilders() { - return new ProtoBuilder(); + return new ProtoRule(supplier("com.google.protobuf.MessageLite.Builder"), "PROTO_BUILDER"); } - private static final Supplier MESSAGE_LITE_BUILDER = - supplier("com.google.protobuf.MessageLite.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 proto builders. */ - private static final class ProtoBuilder extends MethodRule { + /** 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 "PROTO_BUILDER"; + return id; } @Override public Optional evaluateMethod(MethodSymbol method, VisitorState state) { - if (isProtoBuilderType(state, method.owner.type)) { + 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) - && isProtoBuilderType(state, method.getReturnType())) { + && isProtoSubtype(method.getReturnType(), state)) { return Optional.of(ResultUsePolicy.OPTIONAL); } } return Optional.empty(); } - private static boolean isProtoBuilderType(VisitorState state, Type ownerType) { - return ASTHelpers.isSubtype(ownerType, MESSAGE_LITE_BUILDER.get(state), state); + 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"); } } From 70f67b7747b6a8377a506213730865a722b4fc73 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Tue, 10 May 2022 12:59:40 -0700 Subject: [PATCH 50/82] Avoid eager string concatenation when checking Api predicates. PiperOrigin-RevId: 447806474 --- .../bugpatterns/checkreturnvalue/Api.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 index 489ca29c58a..0b02c09907d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -30,6 +30,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CompileTimeConstant; import com.google.errorprone.matchers.Matcher; import com.sun.source.tree.ExpressionTree; import java.util.List; @@ -138,7 +139,11 @@ static Api parse(String apiWithWhitespace) { case ']': // for array signature types break; default: - check(isJavaIdentifierPart(ch), api, "'" + ch + "' is not a valid identifier"); + checkArgument( + isJavaIdentifierPart(ch), + "Unable to parse '%s' because '%s' is not a valid identifier", + api, + ch); } } @@ -167,7 +172,11 @@ static Api parse(String apiWithWhitespace) { // make sure the only thing between the < and > is exactly "init" String constructorName = api.substring(lessThanIndex + 1, greaterThanIndex); - check(constructorName.equals("init"), api, "invalid method name: " + constructorName); + checkArgument( + constructorName.equals("init"), + "Unable to parse '%s' because %s is an invalid method name", + api, + constructorName); } String className = api.substring(0, hashIndex); @@ -231,7 +240,9 @@ static Api parse(String apiWithWhitespace) { return new AutoValue_Api(className, methodName, paramList); } - private static void check(boolean condition, String api, String reason) { + // 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); } } From 53f0eab128276c506db870f11135261381bd1d99 Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Tue, 10 May 2022 13:25:40 -0700 Subject: [PATCH 51/82] Add rule details to `CheckReturnValue` analysis metadata. At some point we'll want to handle this better, but this gets us the data for now. RELNOTES=n/a PiperOrigin-RevId: 447813103 --- .../google/errorprone/bugpatterns/CheckReturnValue.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 b5d1c8436a4..f4e2232ab5b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -150,7 +150,12 @@ public boolean isCovered(ExpressionTree tree, VisitorState state) { return methodToInspect(tree).stream() .flatMap(method -> evaluator.evaluations(method, state)) .findFirst() - .map(evaluation -> ImmutableMap.of("annotation_scope", evaluation.scope())) + .map( + evaluation -> + ImmutableMap.of( + "rule", evaluation.rule(), + "policy", evaluation.policy(), + "scope", evaluation.scope())) .orElse(ImmutableMap.of()); } From fcd523c82387ffe9a5a0c0381f50a8172bbe0b5b Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Tue, 10 May 2022 14:07:09 -0700 Subject: [PATCH 52/82] Fix comment in UBlank PiperOrigin-RevId: 447823447 --- core/src/main/java/com/google/errorprone/refaster/UBlank.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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) { From d14a43b556a809f2a0cbd0f9e5890003d1f1023d Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 11 May 2022 03:40:33 -0700 Subject: [PATCH 53/82] Add a check to require `A.@Nullable B` rather than `@Nullable A.B` for `TYPE_USE` `@Nullable`. PiperOrigin-RevId: 447951336 --- .../NullableOnContainingClass.java | 108 ++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../NullableOnContainingClassTest.java | 99 ++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/NullableOnContainingClass.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/NullableOnContainingClassTest.java 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/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index bd4c2fb69c8..829d5a3fdf9 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -249,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; @@ -703,6 +704,7 @@ public static ScannerSupplier errorChecks() { NonFinalCompileTimeConstant.class, NonRuntimeAnnotation.class, NullTernary.class, + NullableOnContainingClass.class, OptionalEquality.class, OptionalMapUnusedValue.class, OptionalOfRedundantMethod.class, 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(); + } +} From 9ca78dfeb13806571693585d01452e94a210162c Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 11 May 2022 06:17:50 -0700 Subject: [PATCH 54/82] Recognize some experimental Google-internal protobuf annotations. While there, reorganize the two nullness-annotation regexes to make them better parallel one another. PiperOrigin-RevId: 447977257 --- .../nullnesspropagation/NullnessAnnotations.java | 12 +++++++++--- .../nullness/EqualsMissingNullableTest.java | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) 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/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/EqualsMissingNullableTest.java index 78457c41f14..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 @@ -96,6 +96,19 @@ 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() { aggressiveHelper From cc06a4abd6fa834e874f9d9447a8d4863279d56d Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Wed, 11 May 2022 07:48:09 -0700 Subject: [PATCH 55/82] Further optimize Api.parse() by removing whitespace in the parsing pass, and avoiding multiple string copies. PiperOrigin-RevId: 447992096 --- .../bugpatterns/checkreturnvalue/Api.java | 263 +++++++++++------- .../bugpatterns/checkreturnvalue/ApiTest.java | 7 +- 2 files changed, 167 insertions(+), 103 deletions(-) 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 index 0b02c09907d..c9c4678f9cd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -27,7 +27,6 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CompileTimeConstant; @@ -86,8 +85,6 @@ private Matcher matcher() { .withParameters(parameterTypes()); } - private static final Splitter PARAM_SPLITTER = Splitter.on(','); - /** * Parses an API string into an {@link Api}. Example API strings are: * @@ -98,100 +95,161 @@ private Matcher matcher() { *

  • an instance method with types erased (e.g., {@code java.util.List#add(java.lang.Object)}) * */ - static Api parse(String apiWithWhitespace) { - // TODO(kak): consider removing whitespace from the String as we step through the String - String api = whitespace().removeFrom(apiWithWhitespace); - - boolean isConstructor = false; - int hashIndex = -1; - int openParenIndex = -1; - int closeParenIndex = -1; - int lessThanIndex = -1; - int greaterThanIndex = -1; - for (int i = 0; i < api.length(); i++) { - char ch = api.charAt(i); - switch (ch) { - case '#': - check(hashIndex == -1, api, "it contains more than one '#'"); - hashIndex = i; - break; - case '(': - check(openParenIndex == -1, api, "it contains more than one '('"); - openParenIndex = i; - break; - case ')': - check(closeParenIndex == -1, api, "it contains more than one ')'"); - closeParenIndex = i; - break; - case '<': - check(lessThanIndex == -1, api, "it contains more than one '<'"); - lessThanIndex = i; - isConstructor = true; - break; - case '>': - check(greaterThanIndex == -1, api, "it contains more than one '>'"); - greaterThanIndex = i; - isConstructor = true; - break; - case ',': // for separating parameters - case '.': // for package names and fully qualified parameter names - case '[': // for array signature types - case ']': // for array signature types - break; - default: - checkArgument( - isJavaIdentifierPart(ch), - "Unable to parse '%s' because '%s' is not a valid identifier", - api, - ch); - } - } + static Api parse(String api) { + Parser p = new Parser(api); - // make sure we've seen a hash, open paren, and close paren - check(hashIndex != -1, api, "it must contain a '#'"); - check(openParenIndex != -1, api, "it must contain a '('"); - check(closeParenIndex == api.length() - 1, api, "it must end with ')'"); + // 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. - // make sure they came in the correct order: #() - check(hashIndex < openParenIndex, api, "'#' must come before '('"); - check(openParenIndex < closeParenIndex, api, "'(' must come before ')'"); + String className = p.owningType(); + String methodName = p.methodName(); + ImmutableList paramList = p.parameters(); + p.ensureNoMoreCharacters(); - if (isConstructor) { - // make sure that if we've seen a < or >, we also have seen the matching one - check(lessThanIndex != -1, api, "must contain both '<' and '>'"); - check(greaterThanIndex != -1, api, "must contain both '<' and '>'"); + return new AutoValue_Api(className, methodName, paramList); + } - // make sure the < comes directly after the # - check(lessThanIndex == hashIndex + 1, api, "'<' must come directly after '#'"); + private static final class Parser { + private final String api; + private int position = -1; - // make sure that the < comes before the > - check(lessThanIndex < greaterThanIndex, api, "'<' must come before '>'"); + Parser(String api) { + this.api = api; + } - // make sure that the > comes directly before the ( - check(greaterThanIndex == openParenIndex - 1, api, "'>' must come directly before '('"); + 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(); - // make sure the only thing between the < and > is exactly "init" - String constructorName = api.substring(lessThanIndex + 1, greaterThanIndex); - checkArgument( - constructorName.equals("init"), - "Unable to parse '%s' because %s is an invalid method name", + check(!type.isEmpty(), api, "class name cannot be empty"); + check( + isJavaIdentifierStart(type.charAt(0)), api, - constructorName); + "the class name must start with a valid character"); + return type; } - String className = api.substring(0, hashIndex); - String methodName = api.substring(hashIndex + 1, openParenIndex); - String parameters = api.substring(openParenIndex + 1, closeParenIndex); + 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 '>"); - ImmutableList paramList = - parameters.isEmpty() - ? ImmutableList.of() - : PARAM_SPLITTER.splitToStream(parameters).collect(toImmutableList()); + // 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"); + } - // make sure the class name, method name, and parameter names are not empty - check(!className.isEmpty(), api, "the class name cannot be empty"); - check(!methodName.isEmpty(), api, "the method name cannot be empty"); - for (String parameter : paramList) { + 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( @@ -202,8 +260,8 @@ static Api parse(String apiWithWhitespace) { // Array specs must be in balanced pairs at the *end* of the parameter. boolean parsingArrayStart = false; boolean hasArraySpecifiers = false; - for (int i = 1; i < parameter.length(); i++) { - char c = parameter.charAt(i); + for (int k = 1; k < parameter.length(); k++) { + char c = parameter.charAt(k); switch (c) { case '[': check(!parsingArrayStart, api, "multiple consecutive ["); @@ -222,22 +280,27 @@ static Api parse(String apiWithWhitespace) { } } check(!parsingArrayStart, api, "[ without closing ] at the end of a parameter type"); + return parameter; } - // make sure the class name starts with a valid Java identifier character - check( - isJavaIdentifierStart(className.charAt(0)), - api, - "the class name must start with a valid character"); - - if (!isConstructor) { - // make sure the method name starts with a valid Java identifier character - check( - isJavaIdentifierStart(methodName.charAt(0)), - api, - "the method name must start with a valid character"); + + // 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 (whitespace().matches(next)); + return next; } - return new AutoValue_Api(className, methodName, paramList); + void ensureNoMoreCharacters() { + 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 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 index d8660ff028c..3e99528ec75 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/ApiTest.java @@ -16,6 +16,7 @@ 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; @@ -106,15 +107,15 @@ public void parseApi_methodWithoutParams() { @Test public void parseApi_methodWithParamsAndSpaces() { String string = - "com.google.android.libraries.stitch.binder.Binder" - + "#get(android.content.Context,java.lang.Class)"; + " 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(string); + assertThat(api.toString()).isEqualTo(whitespace().removeFrom(string)); } @Test From b603cffc1d5f24ac71d2d187078604481008862e Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Thu, 12 May 2022 09:05:36 -0700 Subject: [PATCH 56/82] Make `CheckReturnValueAnalysis` use the `Api` type to handle its symbol data to ensure consistency between what's in the data and the format we expect from the external API list. Also use erased type for use-site return type in data so we don't get capture-of in the data. PiperOrigin-RevId: 448262603 --- .../bugpatterns/checkreturnvalue/Api.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) 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 index c9c4678f9cd..0b9ce13b500 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -29,9 +29,14 @@ 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.TypeMetadata; +import com.sun.tools.javac.code.Types; import java.util.List; /** @@ -43,6 +48,24 @@ @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 -> erasedType(p.type, types)) + .collect(toImmutableList())); + } + + private static String erasedType(Type type, Types types) { + // Removes type arguments, replacing w/ upper bounds + Type erasedType = types.erasure(type); + Type unannotated = erasedType.cloneWithMetadata(TypeMetadata.EMPTY); // Removes type annotations + return unannotated.toString(); + } + // 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())); @@ -53,16 +76,16 @@ static ImmutableSet createSetFromApis(List apis) { } /** Returns the fully qualified type that contains the given method/constructor. */ - abstract String className(); + 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. */ - abstract String methodName(); + public abstract String methodName(); /** Returns the list of fully qualified parameter types for the given method/constructor. */ - abstract ImmutableList parameterTypes(); + public abstract ImmutableList parameterTypes(); @Override public final String toString() { From 9dee02fa474d469035bfd59decd05133b1230376 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Thu, 12 May 2022 11:45:24 -0700 Subject: [PATCH 57/82] Also check `cancel` calls on `ClosingFuture`. PiperOrigin-RevId: 448301248 --- .../errorprone/bugpatterns/Interruption.java | 3 ++- .../errorprone/bugpatterns/InterruptionTest.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java b/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java index 32f521ba837..56df7e5fe17 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/Interruption.java @@ -54,7 +54,8 @@ public class Interruption extends BugChecker implements MethodInvocationTreeMatc private static final Matcher CANCEL = instanceMethod() - .onDescendantOf("java.util.concurrent.Future") + .onDescendantOfAny( + "java.util.concurrent.Future", "com.google.common.util.concurrent.ClosingFuture") .named("cancel") .withParameters("boolean"); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java index 82a4c6b2111..2c7e66c192e 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/InterruptionTest.java @@ -45,6 +45,21 @@ public void positive() { .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 From 7cf0654405f69d8595fb72d05cee74539836d924 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Thu, 12 May 2022 13:45:36 -0700 Subject: [PATCH 58/82] Recursively erase types and annotations, so that arrays where the element type has annotations are replaced with arrays without those annotations. PiperOrigin-RevId: 448329000 --- .../bugpatterns/checkreturnvalue/Api.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) 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 index 0b9ce13b500..d3bec2f51fd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -35,7 +35,8 @@ 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.TypeMetadata; +import com.sun.tools.javac.code.Type.ArrayType; +import com.sun.tools.javac.code.Type.StructuralTypeMapping; import com.sun.tools.javac.code.Types; import java.util.List; @@ -55,17 +56,36 @@ public static Api fromSymbol(MethodSymbol symbol, VisitorState state) { symbol.owner.getQualifiedName().toString(), symbol.name.toString(), symbol.getParameters().stream() - .map(p -> erasedType(p.type, types)) + .map(p -> fullyErasedAndUnannotatedType(p.type, types)) .collect(toImmutableList())); } - private static String erasedType(Type type, Types types) { + private static String fullyErasedAndUnannotatedType(Type type, Types types) { // Removes type arguments, replacing w/ upper bounds - Type erasedType = types.erasure(type); - Type unannotated = erasedType.cloneWithMetadata(TypeMetadata.EMPTY); // Removes type annotations - return unannotated.toString(); + 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(); + } + + // Remove annotations from all enclosing containers + @Override + public Type visitArrayType(ArrayType t, Void unused) { + return super.visitArrayType((ArrayType) t.baseType(), 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())); From 432dd706048a435b627fdb5ed4e424b7c4e2c991 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Thu, 12 May 2022 14:22:11 -0700 Subject: [PATCH 59/82] Use alternative API to find the parent of a node for precedence finding in Refaster. Fixes #3193 PiperOrigin-RevId: 448337844 --- .../com/google/errorprone/refaster/ExpressionTemplate.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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. // From 2406a4ec31d9cd5875a8253cba0abe7f79bf0ad2 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 12 May 2022 15:50:27 -0700 Subject: [PATCH 60/82] Document that RemoveUnusedImports is usually right PiperOrigin-RevId: 448357463 --- docs/bugpattern/RemoveUnusedImports.md | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/bugpattern/RemoveUnusedImports.md b/docs/bugpattern/RemoveUnusedImports.md index 42e034d3bca..9eb852279c1 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 you 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 {} +``` From 52372a07323228386194211ef599834dcef71a89 Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 13 May 2022 02:30:23 -0700 Subject: [PATCH 61/82] Handle `clear*()` and `merge*()` in `ModifiedButNotUsed`. PiperOrigin-RevId: 448445679 --- .../google/errorprone/bugpatterns/ModifiedButNotUsed.java | 2 +- .../errorprone/bugpatterns/ModifiedButNotUsedTest.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 8aba04b40bd..979dc093d36 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ModifiedButNotUsed.java @@ -119,7 +119,7 @@ public class ModifiedButNotUsed extends BugChecker anyOf( instanceMethod() .onDescendantOf(MESSAGE_BUILDER) - .withNameMatching(Pattern.compile("(add|clear|remove|set|put).+")), + .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/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();", " }", "}") From a82aed198c30cf93b12eb2269cb2a647f6b68236 Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Fri, 13 May 2022 09:11:20 -0700 Subject: [PATCH 62/82] Add rules for `@AutoValue`, `@AutoValue.Builder` and `@AutoBuilder` and remove the rule from `UnusedReturnValueMatcher` that was handling the builders. The handling of `abstract` methods on `@AutoValue`s and `abstract` methods on builders that _aren't_ setters (i.e. one argument, returning the builder type) should match what `IgnoredPureGetter` is doing. RELNOTES=n/a PiperOrigin-RevId: 448509182 --- .../matchers/UnusedReturnValueMatcher.java | 49 +------- .../bugpatterns/CheckReturnValue.java | 6 + .../checkreturnvalue/AutoValueRules.java | 110 ++++++++++++++++++ 3 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/AutoValueRules.java 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/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java index f4e2232ab5b..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,6 +17,9 @@ 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; @@ -82,6 +85,9 @@ public CheckReturnValue(ErrorProneFlags flags) { mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), protoBuilders(), mutableProtos(), + autoValues(), + autoValueBuilders(), + autoBuilders(), externalIgnoreList(), globalDefault(methodPolicy, constructorPolicy)); } 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(); + } + } +} From ad07a50ebdfe0f34b2839d8e4638f5f7eb654d6d Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Fri, 13 May 2022 12:04:12 -0700 Subject: [PATCH 63/82] Fix a typo https://github.com/google/error-prone/commit/745a9a6c7ec322fe2ae8e6af682da46773cb69e7#r73574855 PiperOrigin-RevId: 448548384 --- docs/bugpattern/RemoveUnusedImports.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bugpattern/RemoveUnusedImports.md b/docs/bugpattern/RemoveUnusedImports.md index 9eb852279c1..213e63b34ac 100644 --- a/docs/bugpattern/RemoveUnusedImports.md +++ b/docs/bugpattern/RemoveUnusedImports.md @@ -2,8 +2,8 @@ This import is unused. ## The check reported an import that was actually used! -The check has no known bugs. If you it reports an unused import that looks like -it was actually used, try removing it and recompiling. If everything still +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` From 72fa044023e2607bc2f00f957bb2493cda7b5dfd Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Fri, 13 May 2022 18:25:08 -0700 Subject: [PATCH 64/82] Add exemption for `@DataProvider`,[] PiperOrigin-RevId: 448618147 --- .../java/com/google/errorprone/bugpatterns/UnusedMethod.java | 1 + 1 file changed, 1 insertion(+) 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", From 1c9c1f0aaadc44f80578bde44ffc9f85aa5c69b0 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 16 May 2022 10:41:04 -0700 Subject: [PATCH 65/82] Tolerate methods that return constant expressions in `UnsynchronizedOverridesSynchronized` PiperOrigin-RevId: 448998667 --- .../UnsynchronizedOverridesSynchronized.java | 24 ++++++++++- ...synchronizedOverridesSynchronizedTest.java | 42 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) 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..5d7931857d4 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,10 @@ public Boolean visitBlock(BlockTree tree, Void unused) { @Override public Boolean visitReturn(ReturnTree tree, Void unused) { + ExpressionTree expression = tree.getExpression(); + if (constantExpressions.constantExpression(expression, state).isPresent()) { + return true; + } return scan(tree.getExpression(), null); } 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..1785d4de1bf 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;", @@ -156,4 +160,40 @@ public void ignoreEmptyOverride() { "}") .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(); + } } From a79ec6f0bd1af8e56cf0cfa7f5da85e730a5bd65 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Mon, 16 May 2022 12:28:21 -0700 Subject: [PATCH 66/82] Teach ASTHelpers.targetType not to crash on Object += String. PiperOrigin-RevId: 449026173 --- .../src/main/java/com/google/errorprone/util/ASTHelpers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b3e76ae4ab..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 @@ -1787,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; From 8fcd85423219088eb3ab17fc35ad10df224c806c Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Mon, 16 May 2022 13:28:29 -0700 Subject: [PATCH 67/82] Add a local test to ensure that each API we have in the file is a parseable API, and doesn't contain whitespace. PiperOrigin-RevId: 449040227 --- .../google/errorprone/bugpatterns/checkreturnvalue/Api.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index d3bec2f51fd..53804390230 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -26,6 +26,7 @@ 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; @@ -138,7 +139,8 @@ private Matcher matcher() { *
  • an instance method with types erased (e.g., {@code java.util.List#add(java.lang.Object)}) * */ - static Api parse(String api) { + @VisibleForTesting + public static Api parse(String api) { Parser p = new Parser(api); // Let's parse this in 3 parts: From c480808515e5330b30fd6cc80b40f2f53d7c0958 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Tue, 17 May 2022 13:13:52 -0700 Subject: [PATCH 68/82] When loading the list of APIs that are considered CanIgnoreReturnValue, instead of parsing the strings as Api token objects, use the bare String result instead. This saves a large amount of time during loading of the config, at the cost of a slightly slower evaluation time. Notably: This is now sensitive to whitespace (e.g.: if an API line has leading whitespace or inner whitespace, it may not parse correctly). PiperOrigin-RevId: 449297786 --- .../bugpatterns/checkreturnvalue/Api.java | 28 +++- .../ExternalCanIgnoreReturnValue.java | 147 ++++++++++++------ 2 files changed, 122 insertions(+), 53 deletions(-) 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 index 53804390230..c4303f9531b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -61,7 +61,7 @@ public static Api fromSymbol(MethodSymbol symbol, VisitorState state) { .collect(toImmutableList())); } - private static String fullyErasedAndUnannotatedType(Type type, Types types) { + 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); @@ -141,7 +141,21 @@ private Matcher matcher() { */ @VisibleForTesting public static Api parse(String api) { - Parser p = new Parser(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 # @@ -159,10 +173,12 @@ public static Api parse(String api) { private static final class Parser { private final String api; + private final boolean assumeNoWhitespace; private int position = -1; - Parser(String api) { + Parser(String api, boolean assumeNoWhitespace) { this.api = api; + this.assumeNoWhitespace = assumeNoWhitespace; } String owningType() { @@ -337,11 +353,15 @@ private char nextLookingFor(char delimiter) { checkArgument( position < api.length(), "Could not parse '%s' as it must contain an '%s'", delimiter); next = api.charAt(position); - } while (whitespace().matches(next)); + } 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 ')'"); } 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 index 72e8ea2bcf9..b64fc90de99 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/ExternalCanIgnoreReturnValue.java @@ -16,20 +16,30 @@ 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.LineProcessor; +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 { @@ -42,15 +52,19 @@ public static ResultUseRule externalIgnoreList() { 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_RESOURCE = + private static final Supplier EXTERNAL_RULE_EVALUATOR = VisitorState.memoize( state -> state .errorProneOptions() .getFlags() .get(EXTERNAL_API_EXCLUSION_LIST) - .map(ExternalCanIgnoreReturnValue::tryLoadingConfigFile)); + .map( + filename -> + loadConfigListFromFile(filename, state.errorProneOptions().getFlags())) + .orElse((m, s) -> false)); @Override public String id() { @@ -59,70 +73,105 @@ public String id() { @Override public Optional evaluateMethod(MethodSymbol method, VisitorState state) { - return externallyConfiguredCirvAnnotation(method, state) + return EXTERNAL_RULE_EVALUATOR.get(state).methodMatches(method, state) ? Optional.of(ResultUsePolicy.OPTIONAL) : Optional.empty(); } - public static boolean externallyConfiguredCirvAnnotation(MethodSymbol m, VisitorState s) { - return EXTERNAL_RESOURCE.get(s).map(protoList -> protoList.methodMatches(m, s)).orElse(false); + /** Encapsulates asking "does this API match the list of APIs I care about"? */ + @FunctionalInterface + private interface MethodPredicate { + boolean methodMatches(MethodSymbol methodSymbol, VisitorState state); } - private static FromConfig tryLoadingConfigFile(String filename) { + // 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 { - ImmutableSetMultimap apis = - MoreFiles.asCharSource(Paths.get(filename), UTF_8) - .readLines( - new LineProcessor<>() { - private final ImmutableSetMultimap.Builder collectedApis = - ImmutableSetMultimap.builder(); - - @Override - public boolean processLine(String line) { - Api parsed = Api.parse(line); - collectedApis.put(parsed.className(), parsed); - return true; - } - - @Override - public ImmutableSetMultimap getResult() { - return collectedApis.build(); - } - }); - return new FromConfig(apis); + 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 final class FromConfig { - // TODO(glorioso): Lots of different ways to think about this one here. - // * Do we make Api Comparable and use SortedSet? - // * Index by outer class? - // * Just Set? - private final ImmutableSetMultimap apis; - - private FromConfig(ImmutableSetMultimap apis) { - this.apis = apis; + 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())); + } - boolean methodMatches(MethodSymbol methodSymbol, VisitorState state) { - return apis.get(methodSymbol.enclClass().getQualifiedName().toString()).stream() - .anyMatch(api -> apiMatchesMethodSymbol(methodSymbol, api, state)); - } + private String apiSignature(MethodSymbol methodSymbol, Types types) { + return methodSymbol.owner.getQualifiedName() + + "#" + + methodSymbol.name + + "(" + + paramsString(methodSymbol, types) + + ")"; + } - private static boolean apiMatchesMethodSymbol( - MethodSymbol methodSymbol, Api api, VisitorState state) { - if (!methodSymbol.getSimpleName().contentEquals(api.methodName())) { - return false; + private String paramsString(MethodSymbol symbol, Types types) { + if (symbol.params().isEmpty()) { + return ""; + } + return String.join( + ",", + Iterables.transform( + symbol.params(), p -> fullyErasedAndUnannotatedType(p.type, types))); } + }; + } - // Check for compatibility of these params. - return Iterables.elementsEqual( - api.parameterTypes(), - Iterables.transform( - methodSymbol.params(), p -> state.getTypes().erasure(p.type).toString())); + 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))); } } From 17798c056d8b2c6c698bdd2a5da46a0e34685a64 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Wed, 18 May 2022 12:27:43 -0700 Subject: [PATCH 69/82] Check more carefully for nulls in AbstractReferenceEquality PiperOrigin-RevId: 449546978 --- .../AbstractReferenceEquality.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) 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; From b17c09fe277d3e481aeea85cd4ed2a33f8044111 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Wed, 18 May 2022 13:50:34 -0700 Subject: [PATCH 70/82] Further remove type annotations from non-primitive arrays PiperOrigin-RevId: 449567882 --- .../errorprone/bugpatterns/checkreturnvalue/Api.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 index c4303f9531b..a134c030bbf 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/Api.java @@ -37,7 +37,9 @@ 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; @@ -80,10 +82,15 @@ 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((ArrayType) t.baseType(), unused); + return super.visitArrayType(t.cloneWithMetadata(TypeMetadata.EMPTY), unused); } }; From 85f55f2cff9a121a51e736d42c47bf05fca586f1 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Thu, 19 May 2022 10:18:30 -0700 Subject: [PATCH 71/82] Update the `BugPattern.summary` on `OptionalNotPresent`. PiperOrigin-RevId: 449776888 --- .../com/google/errorprone/bugpatterns/OptionalNotPresent.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 = From 9ff2d42e1fd1f2f7b9505214d5557044d0f0cf65 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Thu, 19 May 2022 12:40:18 -0700 Subject: [PATCH 72/82] Don't try to convert recursive lambdas to methods In principle this is possible to get right, but it's a lot of work for a very rare code pattern. Instead, just give up on it. PiperOrigin-RevId: 449810510 --- .../bugpatterns/UnnecessaryLambda.java | 21 +++++++++++++++++++ .../bugpatterns/UnnecessaryLambdaTest.java | 13 ++++++++++++ 2 files changed, 34 insertions(+) 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 59249528047..bcb19bb6db1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java @@ -190,6 +190,7 @@ private boolean canFix(Tree type, Symbol sym, VisitorState state) { class Scanner extends TreePathScanner { boolean fixable = true; + boolean inInitializer = false; @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { @@ -197,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/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(); + } } From 97a7dbc9c4afb21a18e54edd5b8e98bdff398096 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Thu, 19 May 2022 14:36:03 -0700 Subject: [PATCH 73/82] Don't crash when someone declares a StringBuffer using `var`. PiperOrigin-RevId: 449835264 --- .../errorprone/bugpatterns/JdkObsolete.java | 13 ++++++----- .../bugpatterns/JdkObsoleteTest.java | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) 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/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(); From 2cb3b542010fc3b10c251ad79441f932846990ca Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 20 May 2022 02:39:16 -0700 Subject: [PATCH 74/82] ImmutableChecker: fix the heuristic around MemberSelects. This was previously complaining about something like: x -> { Foo f = new Foo(); f.someList.get(0); // Complains that someList is mutable. } Obviously this doesn't matter, because `f` is owned by the lambda. This would have made the check a bit _over_-zealous. I don't think I saw any false positives in the flumes previously, but then it's probably quite rare for people to access fields directly rather than via a getter. PiperOrigin-RevId: 449938995 --- .../threadsafety/ImmutableChecker.java | 18 ++++---- .../threadsafety/ImmutableCheckerTest.java | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) 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 0666bc07440..77f7f2dbd2d 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 @@ -154,17 +154,17 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { @Override public Void visitMemberSelect(MemberSelectTree tree, Void unused) { - // Special case the access of fields to allow accessing fields which would pass an immutable - // check. + // 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) { + && getSymbol(tree) instanceof VarSymbol + && ((IdentifierTree) tree.getExpression()).getName().contentEquals("this")) { 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 null; } return super.visitMemberSelect(tree, null); } 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 0500e8a12c2..00ba9f3c88f 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 @@ -2525,6 +2525,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 @@ -2809,4 +2828,29 @@ public void lambda_immutableTypeParam() { "}") .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(); + } } From be243a146753075e161672c279f04a781dd7245e Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 20 May 2022 09:42:10 -0700 Subject: [PATCH 75/82] ImmutableChecker: check any variables that anonymous classes close around. PiperOrigin-RevId: 450004419 --- .../threadsafety/ImmutableChecker.java | 259 ++++++++++-------- .../threadsafety/ImmutableCheckerTest.java | 70 +++++ 2 files changed, 209 insertions(+), 120 deletions(-) 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 77f7f2dbd2d..630a7fd3a3a 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 @@ -25,6 +25,7 @@ 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.isSubtype; import static com.google.errorprone.util.ASTHelpers.targetType; import static java.lang.String.format; @@ -47,7 +48,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; @@ -100,6 +100,7 @@ public class ImmutableChecker extends BugChecker private final WellKnownMutability wellKnownMutability; private final ImmutableSet immutableAnnotations; + private final boolean handleAnonymousClasses; ImmutableChecker(ImmutableSet immutableAnnotations) { this(ErrorProneFlags.empty(), immutableAnnotations); @@ -112,6 +113,8 @@ public ImmutableChecker(ErrorProneFlags flags) { private ImmutableChecker(ErrorProneFlags flags, ImmutableSet immutableAnnotations) { this.wellKnownMutability = WellKnownMutability.fromFlags(flags); this.immutableAnnotations = immutableAnnotations; + this.handleAnonymousClasses = + flags.getBoolean("ImmutableChecker:HandleAnonymousClasses").orElse(true); } @Override @@ -128,123 +131,11 @@ public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState 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()) { - effectiveTypeOfThis(symbol, getCurrentPath(), 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(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; } - /** - * 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 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)); @@ -483,6 +374,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 @@ -499,18 +394,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/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java index 00ba9f3c88f..63bf084141f 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 @@ -2853,4 +2853,74 @@ public void chainedGettersAreAcceptable() { "}") .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(); + } } From 98dfcaf8fb3d1eaacc4473bb69f9313483fadf0e Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Sat, 21 May 2022 11:18:46 -0700 Subject: [PATCH 76/82] Scan try-with-resources blocks PiperOrigin-RevId: 450190698 --- .../threadsafety/GuardedByChecker.java | 6 +++++- .../threadsafety/HeldLockAnalyzer.java | 20 +++++++++++-------- .../threadsafety/GuardedByCheckerTest.java | 8 +++----- 3 files changed, 20 insertions(+), 14 deletions(-) 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 5db198ab4d6..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 @@ -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,12 +137,14 @@ 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 @@ -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); 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'", From b822dd45ef437914a1a96faedad414a925026f49 Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 23 May 2022 02:24:16 -0700 Subject: [PATCH 77/82] ImmutableChecker: handle local classes. I feel increasingly guilty at the way this is diverging from ThreadSafety; I think there's a refactor to be had there. PiperOrigin-RevId: 450386803 --- .../threadsafety/ImmutableChecker.java | 6 +++ .../threadsafety/ImmutableCheckerTest.java | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) 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 630a7fd3a3a..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 @@ -25,6 +25,7 @@ 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; @@ -321,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; } 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 63bf084141f..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 From 7cd5def1e6699e09b3d5f25c0d872e6b38d01807 Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Mon, 23 May 2022 13:19:16 -0700 Subject: [PATCH 78/82] Support void placeholder expressions in Refaster PiperOrigin-RevId: 450510643 --- .../google/errorprone/refaster/Template.java | 18 +++++++-- .../refaster/UPlaceholderExpression.java | 2 +- .../refaster/TemplateIntegrationTest.java | 5 +++ ...dExpressionPlaceholderTemplateExample.java | 30 +++++++++++++++ ...dExpressionPlaceholderTemplateExample.java | 30 +++++++++++++++ .../VoidExpressionPlaceholderTemplate.java | 38 +++++++++++++++++++ 6 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/com/google/errorprone/refaster/testdata/input/VoidExpressionPlaceholderTemplateExample.java create mode 100644 core/src/test/java/com/google/errorprone/refaster/testdata/output/VoidExpressionPlaceholderTemplateExample.java create mode 100644 core/src/test/java/com/google/errorprone/refaster/testdata/template/VoidExpressionPlaceholderTemplate.java 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/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/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)); + } +} From 464b21884a31137797e21e4cd2e9574f36737d17 Mon Sep 17 00:00:00 2001 From: Chaoren Lin Date: Tue, 24 May 2022 07:51:49 -0700 Subject: [PATCH 79/82] Implement BanSerializableRead in Android Lint. PiperOrigin-RevId: 450680744 --- .../bugpatterns/BanSerializableRead.java | 24 +--------- .../bugpatterns/SerializableReads.java | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/SerializableReads.java 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/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"); +} From 81acea951cf02fb57286ed5df8744c5cbfe24638 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Tue, 24 May 2022 11:20:46 -0700 Subject: [PATCH 80/82] Don't crash on an "empty" method with a redundant return. PiperOrigin-RevId: 450727959 --- .../bugpatterns/UnsynchronizedOverridesSynchronized.java | 3 ++- .../bugpatterns/UnsynchronizedOverridesSynchronizedTest.java | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 5d7931857d4..f46a0859636 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronized.java @@ -121,7 +121,8 @@ public Boolean visitBlock(BlockTree tree, Void unused) { @Override public Boolean visitReturn(ReturnTree tree, Void unused) { ExpressionTree expression = tree.getExpression(); - if (constantExpressions.constantExpression(expression, state).isPresent()) { + if (expression == null + || constantExpressions.constantExpression(expression, state).isPresent()) { return true; } return scan(tree.getExpression(), null); 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 1785d4de1bf..ce97355f5ff 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnsynchronizedOverridesSynchronizedTest.java @@ -157,6 +157,11 @@ public void ignoreEmptyOverride() { " super.f();", " }", " }", + " class D extends Lib {", + " public void f() {", + " return;", + " }", + " }", "}") .doTest(); } From fbacd85fcf65d2b85468271d1c5975cc00dfa192 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Tue, 24 May 2022 13:42:58 -0700 Subject: [PATCH 81/82] Don't crash when a lambda parameter's type is an inner class in the default package. PiperOrigin-RevId: 450759214 --- .../errorprone/bugpatterns/NonCanonicalType.java | 6 ++++++ .../bugpatterns/NonCanonicalTypeTest.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) 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/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 From 3a2c717f274d512e5a1509b4d91fa4b4bf2f9741 Mon Sep 17 00:00:00 2001 From: amalloy Date: Tue, 24 May 2022 21:37:48 +0000 Subject: [PATCH 82/82] Release Error Prone 2.14.0 --- annotation/pom.xml | 2 +- annotations/pom.xml | 2 +- check_api/pom.xml | 2 +- core/pom.xml | 2 +- docgen/pom.xml | 2 +- docgen_processor/pom.xml | 2 +- pom.xml | 2 +- refaster/pom.xml | 2 +- test_helpers/pom.xml | 2 +- type_annotations/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) 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/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/core/pom.xml b/core/pom.xml index a968768423a..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 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_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/pom.xml b/pom.xml index f6e3708749b..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. 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