From 2577b7294dd31bbbdb688da140e7c29d3619a19b Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 25 May 2022 06:05:45 -0700 Subject: [PATCH 001/102] Remove a TODO. I assume we don't actually want to do this; the cleanup would be pretty annoying, for not much of a benefit. PiperOrigin-RevId: 450906278 --- .../errorprone/bugpatterns/threadsafety/ImmutableChecker.java | 1 - 1 file changed, 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 1337cb52a9a..f33fcf1cef4 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 @@ -259,7 +259,6 @@ public Description matchClass(ClassTree tree, VisitorState state) { if (tree.getSimpleName().length() == 0) { // anonymous classes have empty names - // TODO(cushon): once Java 8 happens, require @Immutable on anonymous classes return handleAnonymousClass(tree, state, analysis); } From 4d700e1852946faf3382ed54ad1cbe4a466c9c2a Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 25 May 2022 06:37:56 -0700 Subject: [PATCH 002/102] Check for direct invocations on mocks. PiperOrigin-RevId: 450910657 --- .../bugpatterns/DirectInvocationOnMock.java | 135 ++++++++++++++ .../threadsafety/ThreadSafety.java | 1 + .../scanner/BuiltInCheckerSuppliers.java | 2 + .../DirectInvocationOnMockTest.java | 174 ++++++++++++++++++ docs/bugpattern/DirectInvocationOnMock.md | 48 +++++ 5 files changed, 360 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java create mode 100644 docs/bugpattern/DirectInvocationOnMock.md diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java new file mode 100644 index 00000000000..92b15bf37e5 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -0,0 +1,135 @@ +/* + * 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.Streams.stream; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.MoreAnnotations.getAnnotationValue; +import static java.lang.String.format; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol.VarSymbol; + +/** A bugpattern; see the description. */ +@BugPattern( + summary = + "Methods should not be directly invoked on mocks. Should this be part of a verify(..)" + + " call?", + severity = WARNING) +public final class DirectInvocationOnMock extends BugChecker implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + ImmutableSet mocks = findMocks(state); + + new SuppressibleTreePathScanner(state) { + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { + Tree parent = + stream(getCurrentPath()) + .skip(1) + .filter(t -> !(t instanceof TypeCastTree)) + .findFirst() + .get(); + var receiver = getReceiver(tree); + if (isMock(receiver) + && !(parent instanceof ExpressionTree + && WHEN.matches((ExpressionTree) parent, state))) { + var description = buildDescription(tree); + if (getCurrentPath().getParentPath().getLeaf() instanceof ExpressionStatementTree) { + var fix = SuggestedFix.builder(); + String verify = qualifyStaticImport("org.mockito.Mockito.verify", fix, state); + description.addFix( + fix.replace(receiver, format("%s(%s)", verify, state.getSourceForNode(receiver))) + .setShortDescription("turn into verify() call") + .build()); + description.addFix( + SuggestedFix.builder() + .delete(tree) + .setShortDescription("delete redundant invocation") + .build()); + } + state.reportMatch(description.build()); + } + return super.visitMethodInvocation(tree, null); + } + + private boolean isMock(ExpressionTree tree) { + var symbol = getSymbol(tree); + return symbol != null + && (mocks.contains(symbol) + || symbol.getAnnotationMirrors().stream() + .filter(am -> am.type.tsym.getQualifiedName().contentEquals("org.mockito.Mock")) + .findFirst() + .filter(am -> getAnnotationValue(am, "answer").isEmpty()) + .isPresent()); + } + }.scan(state.getPath(), null); + + return NO_MATCH; + } + + private ImmutableSet findMocks(VisitorState state) { + ImmutableSet.Builder mocks = ImmutableSet.builder(); + new TreeScanner() { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + if (tree.getInitializer() != null && MOCK.matches(tree.getInitializer(), state)) { + mocks.add(getSymbol(tree)); + } + return super.visitVariable(tree, null); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void unused) { + if (MOCK.matches(tree.getExpression(), state)) { + var symbol = getSymbol(tree.getVariable()); + if (symbol instanceof VarSymbol) { + mocks.add((VarSymbol) symbol); + } + } + return super.visitAssignment(tree, null); + } + }.scan(state.getPath().getCompilationUnit(), null); + return mocks.build(); + } + + private static final Matcher MOCK = + staticMethod().onClass("org.mockito.Mockito").named("mock").withParameters("java.lang.Class"); + + private static final Matcher WHEN = + staticMethod().onClass("org.mockito.Mockito").named("when"); +} diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java index 7231fe529ea..1dbac73529c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java @@ -742,6 +742,7 @@ private AnnotationInfo getAnnotation( if (attr.isPresent()) { ImmutableList containerElements = containerOf(state, attr.get()); if (elementAnnotation != null && containerElements.isEmpty()) { + containerElements = sym.getTypeParameters().stream() .filter(p -> p.getAnnotation(elementAnnotation) != null) 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 829d5a3fdf9..8f980c0e45e 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -100,6 +100,7 @@ import com.google.errorprone.bugpatterns.DepAnn; import com.google.errorprone.bugpatterns.DeprecatedVariable; import com.google.errorprone.bugpatterns.DifferentNameButSame; +import com.google.errorprone.bugpatterns.DirectInvocationOnMock; import com.google.errorprone.bugpatterns.DiscardedPostfixExpression; import com.google.errorprone.bugpatterns.DistinctVarargsChecker; import com.google.errorprone.bugpatterns.DoNotCallChecker; @@ -809,6 +810,7 @@ public static ScannerSupplier errorChecks() { DefaultCharset.class, DefaultPackage.class, DeprecatedVariable.class, + DirectInvocationOnMock.class, DistinctVarargsChecker.class, DoNotCallSuggester.class, DoNotClaimAnnotations.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java new file mode 100644 index 00000000000..6a8419dccd3 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -0,0 +1,174 @@ +/* + * 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.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DirectInvocationOnMock}. */ +@RunWith(JUnit4.class) +public final class DirectInvocationOnMockTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(DirectInvocationOnMock.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(DirectInvocationOnMock.class, getClass()); + + @Test + public void directInvocationOnMock() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " // BUG: Diagnostic contains:", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAssignment() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public void test() {", + " Test test;", + " test = mock(Test.class);", + " // BUG: Diagnostic contains:", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_suggestsVerify() { + refactoring + .addInputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " test.test();", + " }", + "}") + .addOutputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class);", + " verify(test).test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_mockHasExtraOptions_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import org.mockito.Answers;", + "class Test {", + " public void test() {", + " Test test = mock(Test.class, Answers.RETURNS_DEEP_STUBS);", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAnnotatedField() { + helper + .addSourceLines( + "Test.java", + "import org.mockito.Mock;", + "class Test {", + " @Mock public Test test;", + " public void test() {", + " // BUG: Diagnostic contains:", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMockAnnotatedField_mockHasExtraOptions_noFinding() { + helper + .addSourceLines( + "Test.java", + "import org.mockito.Answers;", + "import org.mockito.Mock;", + "class Test {", + " @Mock(answer = Answers.RETURNS_DEEP_STUBS) public Test test;", + " public void test() {", + " test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_withinWhen_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void directInvocationOnMock_withinWhenWithCast_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when((Object) test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } +} diff --git a/docs/bugpattern/DirectInvocationOnMock.md b/docs/bugpattern/DirectInvocationOnMock.md new file mode 100644 index 00000000000..7a981b8ac06 --- /dev/null +++ b/docs/bugpattern/DirectInvocationOnMock.md @@ -0,0 +1,48 @@ +Direct invocations on [mocks](mockito) should be avoided in tests. + +When you call a method on a mock, the call normally does only what you have +configured it to do (through calls to `when(...).thenReturn/thenAnswer`, etc.) +and makes a record of the call that can be read by later `verify(...)` calls. +Both of these are rarely what you want: + +- The reason to configure a mock's behavior is so that the code under test + will react to that behavior. If you want for the test itself to do + something, then do it directly instead of by calling a method on the mock. +- If a future reader of your test sees a call to `verify(foo).bar()`, then the + reader will expect the test to succeed only because the code under test + called `bar()`, not because the test itself did. + +Sometimes, test authors, especially those familiar with other mocking frameworks +(like EasyMock), will call a method on a mock for one of two reasons: + +1. By default, EasyMock requires the test setup to call every method that the + code under test will call. Mockito's defaults do *not* require this. +2. Many EasyMock tests choose to have EasyMock require that the code under test + call *all* the methods that the test setup calls. Mockito tests that want to + verify calls to methods must verify each call individually. To check that + any particular method has been called, a Mockito test must call + `verify(foo).bar()`, and it must do so *after* the code under test has run. + +```java +@Test +public void balanceIsChecked() { + Account account = mock(Account.class); + LoanChecker loanChecker = new LoanChecker(account); + + assertThat(loanChecker.checkEligibility()).isFalse(); + + // Should be verify(account).checkBalance();, or be removed if the call to + // `checkEligibility` is sufficient proof the code is behaving as intended. + account.checkBalance(); +} +``` + +There is at least one edge case in which a call to a mock has different effects: +A call to a `final` method will normally *not* be intercepted by Mockito, so it +will run the implementation of that method in the code under test. Sometimes, +that method will call other methods on the mock object, producing effects +similar to if the test had called those methods directly. Sometimes, the method +will have other effects. Both kinds of effects can be confusing, so prefer to +avoid such calls when possible. + +[mockito]: https://site.mockito.org/ From 700ac8c459757e2a2776134340f797a5abf8e34b Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 25 May 2022 06:46:19 -0700 Subject: [PATCH 003/102] Fix a String.format call. (From https://github.com/google/error-prone/pull/3121, with a regression test added.) PiperOrigin-RevId: 450911920 --- .../bugpatterns/javadoc/InvalidBlockTag.java | 2 +- .../bugpatterns/javadoc/InvalidBlockTagTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java index 4c9dc0dc9fa..d3f7e055e2f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTag.java @@ -192,7 +192,7 @@ public Void visitUnknownBlockTag(UnknownBlockTagTree unknownBlockTagTree, Void u String message = String.format( "@%1$s is not a valid tag, but is a parameter name. " - + "Use {@code %1%s} to refer to parameter names inline.", + + "Use {@code %1$s} to refer to parameter names inline.", tagName); state.reportMatch( buildDescription(diagnosticPosition(getCurrentPath(), state)) diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java index 1349f24b05a..11ee0bf534c 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/InvalidBlockTagTest.java @@ -135,6 +135,21 @@ public void parameterBlockTag() { .doTest(TestMode.TEXT_MATCH); } + @Test + public void parameterBlockTag_finding() { + helper + .addSourceLines( + "Test.java", + "interface Test {", + " /**", + " // BUG: Diagnostic contains: {@code a}", + " * @a blah", + " */", + " void foo(int a);", + "}") + .doTest(); + } + @Test public void inheritDoc() { refactoring From 53a2b119469f29d89bc47861373ea9d58ed4088f Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Wed, 25 May 2022 19:29:02 -0700 Subject: [PATCH 004/102] Internal Change PiperOrigin-RevId: 451069253 --- .../ExternalCanIgnoreReturnValue.java | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) 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 b64fc90de99..cf306796b3b 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 @@ -37,6 +37,7 @@ import com.sun.tools.javac.util.List; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.stream.Stream; @@ -80,7 +81,7 @@ public Optional evaluateMethod(MethodSymbol method, VisitorStat /** Encapsulates asking "does this API match the list of APIs I care about"? */ @FunctionalInterface - private interface MethodPredicate { + interface MethodPredicate { boolean methodMatches(MethodSymbol methodSymbol, VisitorState state); } @@ -90,25 +91,25 @@ private interface MethodPredicate { enum ConfigParser { AS_STRINGS { @Override - MethodPredicate load(CharSource file) throws IOException { - return configByInterpretingMethodsAsStrings(file); + MethodPredicate load(Path file) throws IOException { + return configByInterpretingMethodsAsStrings(MoreFiles.asCharSource(file, UTF_8)); } }, PARSE_TOKENS { @Override - MethodPredicate load(CharSource file) throws IOException { - return configByParsingApiObjects(file); + MethodPredicate load(Path file) throws IOException { + return configByParsingApiObjects(MoreFiles.asCharSource(file, UTF_8)); } }; - abstract MethodPredicate load(CharSource file) throws IOException; + abstract MethodPredicate load(Path file) throws IOException; } private static MethodPredicate loadConfigListFromFile(String filename, ErrorProneFlags flags) { ConfigParser configParser = flags.getEnum(EXCLUSION_LIST_PARSER, ConfigParser.class).orElse(ConfigParser.AS_STRINGS); try { - CharSource file = MoreFiles.asCharSource(Paths.get(filename), UTF_8); + Path file = Paths.get(filename); return configParser.load(file); } catch (IOException e) { throw new UncheckedIOException( @@ -133,20 +134,7 @@ public boolean methodMatches(MethodSymbol methodSymbol, VisitorState state) { private String apiSignature(MethodSymbol methodSymbol, Types types) { return methodSymbol.owner.getQualifiedName() + "#" - + methodSymbol.name - + "(" - + paramsString(methodSymbol, types) - + ")"; - } - - private String paramsString(MethodSymbol symbol, Types types) { - if (symbol.params().isEmpty()) { - return ""; - } - return String.join( - ",", - Iterables.transform( - symbol.params(), p -> fullyErasedAndUnannotatedType(p.type, types))); + + methodNameAndParams(methodSymbol, types); } }; } @@ -160,7 +148,7 @@ private static MethodPredicate configByParsingApiObjects(CharSource file) throws .collect(toImmutableSetMultimap(Api::className, api -> api)); } return (methodSymbol, state) -> - apis.get(methodSymbol.enclClass().getQualifiedName().toString()).stream() + apis.get(surroundingClass(methodSymbol)).stream() .anyMatch( api -> methodSymbol.getSimpleName().contentEquals(api.methodName()) @@ -168,10 +156,26 @@ && methodParametersMatch( api.parameterTypes(), methodSymbol.params(), state.getTypes())); } + static String surroundingClass(MethodSymbol methodSymbol) { + return methodSymbol.enclClass().getQualifiedName().toString(); + } + + static String methodNameAndParams(MethodSymbol methodSymbol, Types types) { + return methodSymbol.name + "(" + paramsString(types, methodSymbol.params()) + ")"; + } + private static boolean methodParametersMatch( ImmutableList parameters, List methodParams, Types types) { return Iterables.elementsEqual( parameters, Iterables.transform(methodParams, p -> fullyErasedAndUnannotatedType(p.type, types))); } + + private static String paramsString(Types types, List params) { + if (params.isEmpty()) { + return ""; + } + return String.join( + ",", Iterables.transform(params, p -> fullyErasedAndUnannotatedType(p.type, types))); + } } From 069e836ea40b50593fca64fb6f5236331cedfc1d Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 26 May 2022 09:33:25 -0700 Subject: [PATCH 005/102] Ban `T extends Object`. PiperOrigin-RevId: 451180284 --- .../bugpatterns/nullness/ExtendsObject.java | 60 ++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../nullness/ExtendsObjectTest.java | 63 +++++++++++++++++++ docs/bugpattern/ExtendsObject.md | 6 ++ 4 files changed, 131 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java create mode 100644 docs/bugpattern/ExtendsObject.md diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java new file mode 100644 index 00000000000..62041841aca --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/ExtendsObject.java @@ -0,0 +1,60 @@ +/* + * 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.nullness; + +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getType; +import static java.lang.String.format; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.TypeParameterTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.fixes.SuggestedFixes; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeParameterTree; + +/** A bugpattern: see the summary. */ +@BugPattern( + summary = + "`T extends Object` is redundant in normal Java, and does not work to describe `T` as" + + " non-null across compilation boundaries when the Checker Framework unless you" + + " compile users against bytecode generated by the Checker Framework javac. (If you" + + " are building this code with the Checker Framework javac, then disable this check.)", + severity = SeverityLevel.WARNING) +public final class ExtendsObject extends BugChecker implements TypeParameterTreeMatcher { + private static final String NON_NULL = "org.checkerframework.checker.nullness.qual.NonNull"; + + @Override + public Description matchTypeParameter(TypeParameterTree tree, VisitorState state) { + for (Tree bound : tree.getBounds()) { + if (!state.getTypes().isSameType(getType(bound), state.getSymtab().objectType)) { + continue; + } + if (!(bound instanceof AnnotatedTypeTree)) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + String nonNull = SuggestedFixes.qualifyType(state, fix, NON_NULL); + return describeMatch(bound, fix.prefixWith(bound, format(" @%s ", nonNull)).build()); + } + } + return 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 8f980c0e45e..73c4bce4f87 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -491,6 +491,7 @@ import com.google.errorprone.bugpatterns.javadoc.UrlInSee; import com.google.errorprone.bugpatterns.nullness.EqualsBrokenForNull; import com.google.errorprone.bugpatterns.nullness.EqualsMissingNullable; +import com.google.errorprone.bugpatterns.nullness.ExtendsObject; import com.google.errorprone.bugpatterns.nullness.FieldMissingNullable; import com.google.errorprone.bugpatterns.nullness.ParameterMissingNullable; import com.google.errorprone.bugpatterns.nullness.ReturnMissingNullable; @@ -827,6 +828,7 @@ public static ScannerSupplier errorChecks() { ErroneousThreadPoolConstructorChecker.class, EscapedEntity.class, ExtendingJUnitAssert.class, + ExtendsObject.class, FallThrough.class, Finally.class, FloatCast.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java new file mode 100644 index 00000000000..009decb7115 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ExtendsObjectTest.java @@ -0,0 +1,63 @@ +/* + * 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.nullness; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ExtendsObject}. */ +@RunWith(JUnit4.class) +public final class ExtendsObjectTest { + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(ExtendsObject.class, getClass()); + + @Test + public void positive() { + helper + .addInputLines( + "Test.java", // + "class Foo {}") + .addOutputLines( + "Test.java", // + "import org.checkerframework.checker.nullness.qual.NonNull;", + "class Foo {}") + .doTest(); + } + + @Test + public void extendsParameterWithObjectErasure_noFinding() { + helper + .addInputLines( + "Test.java", // + "class Foo {}") + .expectUnchanged() + .doTest(); + } + + @Test + public void negative() { + helper + .addInputLines( + "Test.java", // + "import org.checkerframework.checker.nullness.qual.NonNull;", + "class Foo {}") + .expectUnchanged() + .doTest(); + } +} diff --git a/docs/bugpattern/ExtendsObject.md b/docs/bugpattern/ExtendsObject.md new file mode 100644 index 00000000000..c06c5369a6d --- /dev/null +++ b/docs/bugpattern/ExtendsObject.md @@ -0,0 +1,6 @@ +`T extends Object` is redundant when using normal (non-Checker Framework +checked) code. + +However, `T extends Object` compiles to the same bytecode as `T` when using +vanilla javac. So, when using Checker on vanilla javac's bytecode, `T extends +Object` does not imply non-null bounds *outside the same compilation unit*. From 750ca26fd418f978e1488eaee6ee0f8193f6d2ed Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Thu, 26 May 2022 12:02:27 -0700 Subject: [PATCH 006/102] [] #checkreturnvalue PiperOrigin-RevId: 451213999 --- .../src/main/java/com/google/errorprone/refaster/Refaster.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/refaster/Refaster.java b/core/src/main/java/com/google/errorprone/refaster/Refaster.java index 44776dd461f..96b72f11f77 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Refaster.java +++ b/core/src/main/java/com/google/errorprone/refaster/Refaster.java @@ -16,6 +16,8 @@ package com.google.errorprone.refaster; +import com.google.errorprone.annotations.CanIgnoreReturnValue; + /** * Static utilities to indicate special handling in Refaster templates. * @@ -68,6 +70,7 @@ public static T[] asVarargs(T arg) { * */ @SafeVarargs + @CanIgnoreReturnValue public static T anyOf(T... expressions) { throw new UnsupportedOperationException(); } From 6821fe171f5cd75deb388fe68ec3b1710292420e Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 27 May 2022 02:47:27 -0700 Subject: [PATCH 007/102] Delete deprecated constructors. PiperOrigin-RevId: 451345981 --- .../threadsafety/ThreadSafety.java | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java index 1dbac73529c..2c80a2d9b40 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java @@ -232,47 +232,6 @@ public ThreadSafety build(VisitorState state) { } } - /** Use {@link #builder()} instead. */ - // TODO(ghm): Delete after a JB release. - @Deprecated - public ThreadSafety( - VisitorState state, - KnownTypes knownTypes, - Set markerAnnotations, - Set acceptedAnnotations, - @Nullable Class containerOfAnnotation, - @Nullable Class suppressAnnotation) { - this( - state, - knownTypes, - markerAnnotations, - acceptedAnnotations, - containerOfAnnotation, - suppressAnnotation, - /* typeParameterAnnotation= */ null); - } - - /** Use {@link #builder()} instead. */ - @Deprecated - public ThreadSafety( - VisitorState state, - KnownTypes knownTypes, - Set markerAnnotations, - Set acceptedAnnotations, - @Nullable Class containerOfAnnotation, - @Nullable Class suppressAnnotation, - @Nullable Class typeParameterAnnotation) { - this( - state, - Purpose.FOR_IMMUTABLE_CHECKER, - knownTypes, - markerAnnotations, - acceptedAnnotations, - containerOfAnnotation, - suppressAnnotation, - typeParameterAnnotation); - } - private ThreadSafety( VisitorState state, Purpose purpose, From 71484cc6449d408a008ac016567fcc2ce35be0d6 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Fri, 27 May 2022 16:26:31 -0700 Subject: [PATCH 008/102] Create `CanIgnoreReturnValueSuggester`, which recommends annotating methods which always `return this;` with `@CanIgnoreReturnValue`. The check is currently disabled (pending cleanup); afterwards, we will upgrade it to a warning (to encourage new APIs to annotate themselves properly). #checkreturnvalue PiperOrigin-RevId: 451506426 --- .../CanIgnoreReturnValueSuggester.java | 170 ++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../CanIgnoreReturnValueSuggesterTest.java | 314 ++++++++++++++++++ 3 files changed, 486 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java new file mode 100644 index 00000000000..91736efb3c4 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -0,0 +1,170 @@ +/* + * 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.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +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.isVoidType; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.suppliers.Supplier; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Checker that recommends annotating a method with {@code @CanIgnoreReturnValue} if the method + * always returns {@code this}. + */ +@BugPattern( + summary = "Methods that always 'return this' should be annotated with @CanIgnoreReturnValue", + severity = WARNING) +public final class CanIgnoreReturnValueSuggester extends BugChecker implements MethodTreeMatcher { + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; + + private static final Supplier PROTO_BUILDER = + VisitorState.memoize(s -> s.getTypeFromString("com.google.protobuf.MessageLite.Builder")); + + // TODO(kak): catch places where an input parameter is always returned + + // TODO(b/202772719): catch cases where a method delegates to a CIRV method. E.g.: + // + // public Builder addFoos(Foo... foos) { + // return addFoos(asList(foos)); + // } + // @CanIgnoreReturnValue + // public Builder addFoos(Iterable foos) { + // this.foos = checkNotNull(foos); + // return this; + // } + // + // This would also catch the `return self();` pattern and `return thisFoo();` patterns that we + // sometimes see (b/202772578#comment6 and b/202772578#comment7). + // All of this would work better if we had multi-pass, otherwise newly annotated @CIRV methods + // won't "propagate" until the next analysis pass. + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + MethodSymbol methodSymbol = getSymbol(methodTree); + // if the method is static or doesn't have a body, it can't possibly "return this", bail out + if (methodSymbol.isStatic() || methodTree.getBody() == null) { + return Description.NO_MATCH; + } + + // if the method is a proto builder method, bail out + if (isProtoBuilderSubtype(methodSymbol.owner.type, state)) { + // TODO(kak): could we just use this instead? + // ProtoRules.protoBuilders().evaluate(methodSymbol, state).isPresent() + return Description.NO_MATCH; + } + + // if the method is already directly annotated w/ @CIRV, bail out + if (hasAnnotation(methodTree, CIRV, state)) { + return Description.NO_MATCH; + } + + // if the enclosing type is already annotated with CIRV, we could theoretically _not_ directly + // annotate the method but we're likely to discourage annotating types with CIRV: b/229776283 + + // if the method is already directly annotated w/ @CRV, bail out + if (hasAnnotation(methodTree, CRV, state)) { + // TODO(kak): we might want to actually _remove_ @CRV and add @CIRV in this case! + return Description.NO_MATCH; + } + + // if the method is a constructor or has a void/Void return type, bail out + Tree returnType = methodTree.getReturnType(); + if (returnType == null /* constructors have a null returnType */ + || isVoidType(getType(returnType), state)) { + // TODO(b/234176673): isVoidType() also flags the `Void` (capital) type; is that desired? + return Description.NO_MATCH; + } + + AtomicBoolean allReturnThis = new AtomicBoolean(true); + AtomicBoolean atLeastOneReturn = new AtomicBoolean(false); + + new TreePathScanner() { + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn.set(true); + if (!returnsThis(returnTree)) { + allReturnThis.set(false); + // once we've set allReturnThis to false, no need to descend further + return null; + } + return super.visitReturn(returnTree, null); + } + + /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */ + private boolean returnsThis(ReturnTree returnTree) { + ExpressionTree returnExpression = returnTree.getExpression(); + if (returnExpression instanceof IdentifierTree) { + if (((IdentifierTree) returnExpression).getName().contentEquals("this")) { + return true; + } + } + return false; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into new classes + return null; + } + }.scan(state.getPath(), null); + + if (atLeastOneReturn.get() && allReturnThis.get()) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + String cirvName = qualifyType(state, fix, CIRV); + + // we could add a trailing comment (e.g., @CanIgnoreReturnValue // returns `this`), but all + // developers will become familiar with these annotations sooner or later + fix.prefixWith(methodTree, "@" + cirvName + "\n"); + + return describeMatch(methodTree, fix.build()); + } + return Description.NO_MATCH; + } + + private static boolean isProtoBuilderSubtype(Type ownerType, VisitorState state) { + return isSubtype(ownerType, PROTO_BUILDER.get(state), state); + } +} 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 73c4bce4f87..e7da8d5f693 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -415,6 +415,7 @@ import com.google.errorprone.bugpatterns.argumentselectiondefects.ArgumentSelectionDefectChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AssertEqualsArgumentOrderChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AutoValueConstructorOrderChecker; +import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; import com.google.errorprone.bugpatterns.checkreturnvalue.UsingJsr305CheckReturnValue; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionIncompatibleType; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionUndefinedEquality; @@ -1014,6 +1015,7 @@ public static ScannerSupplier errorChecks() { BinderIdentityRestoredDangerously.class, // TODO: enable this by default. BindingToUnqualifiedCommonType.class, BooleanParameter.class, + CanIgnoreReturnValueSuggester.class, CatchingUnchecked.class, CheckedExceptionNotThrown.class, ClassName.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java new file mode 100644 index 00000000000..64e9b687a70 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -0,0 +1,314 @@ +/* + * 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.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link CanIgnoreReturnValueSuggester}. */ +@RunWith(JUnit4.class) +public class CanIgnoreReturnValueSuggesterTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(CanIgnoreReturnValueSuggester.class, getClass()); + + @Test + public void testSimpleCase() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testSimpleCaseAlreadyAnnotatedWithCirv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseAlreadyAnnotatedWithCrv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "public final class Client {", + " @CheckReturnValue", // this is "wrong" -- the checker could fix it though! + " public Client getValue() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseWithNestedLambda() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import java.util.function.Function;", + "public final class Client {", + " public Client getValue() {", + " new Function() {", + " @Override", + " public String apply(String in) {", + " return \"kurt\";", + " }", + " };", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import java.util.function.Function;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " new Function() {", + " @Override", + " public String apply(String in) {", + " return \"kurt\";", + " }", + " };", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testAnotherMethodDoesntReturnThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue1() {", + " return this;", + " }", + " public Client getValue2() {", + " return new Client();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue1() {", + " return this;", + " }", + " public Client getValue2() {", + " return new Client();", + " }", + "}") + .doTest(); + } + + @Test + public void testNestedCase() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " if (true) {", + " return new Client();", + " }", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testNestedCaseBothReturningThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " if (true) {", + " return this;", + " }", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " if (true) {", + " return this;", + " }", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testCapitalVoidReturnType() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Void getValue() {", + " return null;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testLowerVoidReturnType() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Void getValue() {", + " return null;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testConstructor() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client() {", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSometimesThrows() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " if (true) throw new UnsupportedOperationException();", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " if (true) throw new UnsupportedOperationException();", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testAlwaysThrows() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " throw new UnsupportedOperationException();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSimpleCaseWithSimpleNameConflict() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public @interface CanIgnoreReturnValue {}", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public @interface CanIgnoreReturnValue {}", + " @com.google.errorprone.annotations.CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(); + } +} From ad0289b0251293d73c5ea62a5f165bba8577b3a7 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Tue, 31 May 2022 14:53:28 -0700 Subject: [PATCH 009/102] Discard vacuous refactoring results Filtering out no-op refactoring operations avoids unnecessary build output and file I/O. Fixes #1562. Fixes #3236 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/error-prone/pull/3236 from PicnicSupermarket:improvement/discard-vacuous-refactorings b50990f3097172e687719b7b31adde2119e39795 PiperOrigin-RevId: 452146797 --- .../errorprone/RefactoringCollection.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java b/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java index 62d4a76208a..ced2a69e834 100644 --- a/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java +++ b/check_api/src/main/java/com/google/errorprone/RefactoringCollection.java @@ -160,12 +160,11 @@ public DescriptionListener getDescriptionListener(Log log, JCCompilationUnit com RefactoringResult applyChanges(URI uri) throws Exception { Collection listeners = foundSources.removeAll(uri); - if (listeners.isEmpty()) { - return RefactoringResult.create("", RefactoringResultType.NO_CHANGES); + if (doApplyProcess(fileDestination, new FsFileSource(rootPath), listeners)) { + return postProcess.apply(uri); } - doApplyProcess(fileDestination, new FsFileSource(rootPath), listeners); - return postProcess.apply(uri); + return RefactoringResult.create("", RefactoringResultType.NO_CHANGES); } private static void writePatchFile( @@ -185,15 +184,21 @@ private static void writePatchFile( } } - private static void doApplyProcess( + private static boolean doApplyProcess( FileDestination fileDestination, FileSource fileSource, Collection listeners) { + boolean appliedDiff = false; for (DelegatingDescriptionListener listener : listeners) { + if (listener.base.isEmpty()) { + continue; + } + try { SourceFile file = fileSource.readFile(listener.base.getRelevantFileName()); listener.base.applyDifferences(file); fileDestination.writeFile(file); + appliedDiff = true; } catch (IOException e) { logger.log( Level.WARNING, @@ -201,6 +206,8 @@ private static void doApplyProcess( e); } } + + return appliedDiff; } private static final class DelegatingDescriptionListener implements DescriptionListener { From 32fb22a2cd87eb2a1ee4db03eb4be228d8b7a8a2 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Wed, 1 Jun 2022 14:05:59 -0700 Subject: [PATCH 010/102] Fix typo in `CanIgnoreReturnValueSuggester`. PiperOrigin-RevId: 452380026 --- .../checkreturnvalue/CanIgnoreReturnValueSuggester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java index 91736efb3c4..7bc9ab31982 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -146,7 +146,7 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { @Override public Void visitNewClass(NewClassTree node, Void unused) { - // don't descend into new classes + // don't descend into declarations of anonymous classes return null; } }.scan(state.getPath(), null); From c81f12e0d8b94f647b794cecfa713053022d42ed Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Wed, 1 Jun 2022 15:49:15 -0700 Subject: [PATCH 011/102] Add a test that demonstrates the issue with unqualified static field references in the `@InlineMe` `Suggester`. #inlineme PiperOrigin-RevId: 452402823 --- .../bugpatterns/inlineme/SuggesterTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java index bde02f4c9ab..bf6d4e2638d 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/inlineme/SuggesterTest.java @@ -118,6 +118,64 @@ public void testStaticMethodInNewClass() { .doTest(); } + @Test + public void testUnqualifiedStaticFieldReference() { + refactoringTestHelper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @Deprecated", + " public int stringLength() {", + " return STR.length();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.InlineMe;", + "public final class Client {", + " public static final String STR = \"kurt\";", + // TODO(b/234643232): this is a bug; it should be "Client.STR.length()" plus an import + " @InlineMe(replacement = \"STR.length()\")", + " @Deprecated", + " public int stringLength() {", + " return STR.length();", + " }", + "}") + .doTest(); + } + + @Test + public void testQualifiedStaticFieldReference() { + refactoringTestHelper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @Deprecated", + " public int stringLength() {", + " return Client.STR.length();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.InlineMe;", + "public final class Client {", + " public static final String STR = \"kurt\";", + " @InlineMe(replacement = \"Client.STR.length()\", " + + "imports = \"com.google.frobber.Client\")", + " @Deprecated", + " public int stringLength() {", + " return Client.STR.length();", + " }", + "}") + .doTest(); + } + @Test public void testProtectedConstructor() { refactoringTestHelper From c7412f5f1a656d29b0dac6841d3af56fa1de2edf Mon Sep 17 00:00:00 2001 From: Wes Alvaro Date: Mon, 6 Jun 2022 03:35:37 -0700 Subject: [PATCH 012/102] Document that type-use annotations are not supported in Dagger modules. In the future, we could use the previous snapshot's code to to enable them. ``` // Method return value is annotated as Nullable -> No match for (AnnotationMirror mirror : enclosingMethodSym.getReturnType().getAnnotationMirrors()) { if (mirror.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) { return Description.NO_MATCH; } } ``` PiperOrigin-RevId: 453161899 --- .../inject/dagger/ProvidesNull.java | 10 ++++- .../inject/dagger/ProvidesNullTest.java | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNull.java b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNull.java index 58f8b53db1a..c25e09a4db9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNull.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNull.java @@ -72,10 +72,16 @@ public Description matchReturn(ReturnTree returnTree, VisitorState state) { } MethodSymbol enclosingMethodSym = ASTHelpers.getSymbol(enclosingMethod); - if (!ASTHelpers.hasAnnotation(enclosingMethodSym, "dagger.Provides", state) - || ASTHelpers.hasDirectAnnotationWithSimpleName(enclosingMethodSym, "Nullable")) { + // Method is not annotated as Provides -> No match + if (!ASTHelpers.hasAnnotation(enclosingMethodSym, "dagger.Provides", state)) { return Description.NO_MATCH; } + // Method is annotated as Nullable -> No match + if (ASTHelpers.hasDirectAnnotationWithSimpleName(enclosingMethodSym, "Nullable")) { + return Description.NO_MATCH; + } + // Type-use annotations do *NOT* work with Dagger. See b/117251022 + // You must use *any* non-type-use Nullable annotation. Fix addNullableFix = SuggestedFix.builder() diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java index 54d44d1c4d1..9bed64321d6 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/inject/dagger/ProvidesNullTest.java @@ -79,6 +79,43 @@ public void hasOtherNullable() { .doTest(); } + @Test + public void hasTypeUseNullableOnMethod() { + compilationHelper + .addSourceLines( + "Test.java", + "import dagger.Provides;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " @Provides", + " @Nullable", + " public Object providesObject() {", + " // BUG: Diagnostic contains: Did you mean '@Nullable' or 'throw new" + + " RuntimeException();'", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void hasTypeUseNullableOnReturnType() { + compilationHelper + .addSourceLines( + "Test.java", + "import dagger.Provides;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " @Provides", + " public @Nullable Object providesObject() {", + " // BUG: Diagnostic contains: Did you mean '@Nullable' or 'throw new" + + " RuntimeException();'", + " return null;", + " }", + "}") + .doTest(); + } + /** * Tests that we do not flag Guice {@code @Provides} methods. While this is also wrong, there is * no enforcement in Guice and so the incorrect usage is too common to error on. From a74361efe9cbbccc38f5d2e221c7f98c431e62f5 Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 6 Jun 2022 07:08:38 -0700 Subject: [PATCH 013/102] DirectInvocationOnMock: just exempt methods named `when` in general. PiperOrigin-RevId: 453192049 --- .../bugpatterns/DirectInvocationOnMock.java | 4 ++-- .../DirectInvocationOnMockTest.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java index 92b15bf37e5..73e578ee5ed 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -20,6 +20,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.anyMethod; import static com.google.errorprone.matchers.Matchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.getReceiver; import static com.google.errorprone.util.ASTHelpers.getSymbol; @@ -130,6 +131,5 @@ public Void visitAssignment(AssignmentTree tree, Void unused) { private static final Matcher MOCK = staticMethod().onClass("org.mockito.Mockito").named("mock").withParameters("java.lang.Class"); - private static final Matcher WHEN = - staticMethod().onClass("org.mockito.Mockito").named("when"); + private static final Matcher WHEN = anyMethod().anyClass().named("when"); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java index 6a8419dccd3..2f3403372d1 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -155,6 +155,26 @@ public void directInvocationOnMock_withinWhen_noFinding() { .doTest(); } + @Test + public void directInvocationOnMock_withinCustomWhen_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import org.mockito.stubbing.OngoingStubbing;", + "class Test {", + " public OngoingStubbing when(T t) {", + " return org.mockito.Mockito.when(t);", + " }", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenReturn(null);", + " return null;", + " }", + "}") + .doTest(); + } + @Test public void directInvocationOnMock_withinWhenWithCast_noFinding() { helper From f77f37e12cadfe31295c863649809a07328aa32a Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 7 Jun 2022 10:23:28 -0700 Subject: [PATCH 014/102] Create `SelfAlwaysReturnsThis` which requires that non-static, non-void, non-abstract methods named `self()` that return the enclosing class always `return this`. PiperOrigin-RevId: 453468471 --- .../bugpatterns/SelfAlwaysReturnsThis.java | 144 ++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../SelfAlwaysReturnsThisTest.java | 219 ++++++++++++++++++ docs/bugpattern/SelfAlwaysReturnsThis.md | 29 +++ 4 files changed, 394 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java create mode 100644 docs/bugpattern/SelfAlwaysReturnsThis.md diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java new file mode 100644 index 00000000000..3ac60a270a9 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java @@ -0,0 +1,144 @@ +/* + * 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.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.isSameType; +import static com.google.errorprone.util.ASTHelpers.isVoidType; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; + +/** + * Non-abstract instance methods named {@code self()} that return the enclosing class must always + * {@code return this}. + */ +@BugPattern( + summary = + "Non-abstract instance methods named 'self()' that return the enclosing class must always" + + " 'return this'", + severity = WARNING) +public final class SelfAlwaysReturnsThis extends BugChecker implements MethodTreeMatcher { + + // TODO(kak): can we use a MethodMatcher instead? Or is that only for MethodInvocationTree's? + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + MethodSymbol methodSymbol = getSymbol(methodTree); + + // The method must: + // * not be a constructor + // * be named `self` + // * have no params + // * be an instance method (not static) + // * have a body (not abstract) + if (methodSymbol.isConstructor() + || !methodSymbol.getSimpleName().contentEquals("self") + || !methodSymbol.getParameters().isEmpty() + || methodSymbol.isStatic() + || methodTree.getBody() == null) { + return Description.NO_MATCH; + } + + // * not have a void (or Void) return type + Tree returnType = methodTree.getReturnType(); + if (isVoidType(getType(returnType), state)) { + return Description.NO_MATCH; + } + + // * have the same return type as the enclosing type + if (!isSameType(getType(returnType), enclosingClass(methodSymbol).type, state)) { + return Description.NO_MATCH; + } + + // * have a body that is exactly 1 statement + if (methodTree.getBody().getStatements().size() == 1) { + + // * the 1 statement is "return this;" or "return (T) this;" + StatementTree statement = methodTree.getBody().getStatements().get(0); + if (statement instanceof ReturnTree) { + ExpressionTree returnExpression = ((ReturnTree) statement).getExpression(); + + // e.g., `return this;` + if (isThis(returnExpression)) { + return Description.NO_MATCH; + } + + // e.g., `return (T) this;` + if (returnExpression instanceof TypeCastTree) { + TypeCastTree typeCastTree = (TypeCastTree) returnExpression; + if (isThis(typeCastTree.getExpression())) { + return Description.NO_MATCH; + } + } + } + } + + // * or have a body that is exactly 2 statement + if (methodTree.getBody().getStatements().size() == 2) { + + // * the 1st statement is an assignment (e.g., Builder self = (Builder) this;) + StatementTree firstStatement = methodTree.getBody().getStatements().get(0); + if (firstStatement instanceof VariableTree) { + VariableTree variableTree = (VariableTree) firstStatement; + if (variableTree.getInitializer() instanceof TypeCastTree) { + TypeCastTree typeCastTree = (TypeCastTree) variableTree.getInitializer(); + if (isThis(typeCastTree.getExpression())) { + VarSymbol assignedVariable = getSymbol(variableTree); + + // * the 2nd statement is a return of the previous variable (e.g., return self;) + StatementTree secondStatement = methodTree.getBody().getStatements().get(1); + if (secondStatement instanceof ReturnTree) { + ReturnTree returnTree = (ReturnTree) secondStatement; + if (assignedVariable != null + && assignedVariable.equals(getSymbol(returnTree.getExpression()))) { + return Description.NO_MATCH; + } + } + } + } + } + } + + return describeMatch( + methodTree, SuggestedFix.replace(methodTree.getBody(), "{ return this; }")); + } + + /** Returns whether or not the given {@link ExpressionTree} is exactly {@code this}. */ + private static boolean isThis(ExpressionTree expression) { + if (expression instanceof IdentifierTree) { + return ((IdentifierTree) expression).getName().contentEquals("this"); + } + return false; + } +} 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 e7da8d5f693..c2c98b69100 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -302,6 +302,7 @@ import com.google.errorprone.bugpatterns.RobolectricShadowDirectlyOn; import com.google.errorprone.bugpatterns.RxReturnValueIgnored; import com.google.errorprone.bugpatterns.SameNameButDifferent; +import com.google.errorprone.bugpatterns.SelfAlwaysReturnsThis; import com.google.errorprone.bugpatterns.SelfAssignment; import com.google.errorprone.bugpatterns.SelfComparison; import com.google.errorprone.bugpatterns.SelfEquals; @@ -949,6 +950,7 @@ public static ScannerSupplier errorChecks() { RxReturnValueIgnored.class, SameNameButDifferent.class, ScopeAnnotationOnInterfaceOrAbstractClass.class, + SelfAlwaysReturnsThis.class, ShortCircuitBoolean.class, StaticAssignmentInConstructor.class, StaticAssignmentOfThrowable.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java new file mode 100644 index 00000000000..9b338b30418 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java @@ -0,0 +1,219 @@ +/* + * 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.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link SelfAlwaysReturnsThis}. */ +@RunWith(JUnit4.class) +public class SelfAlwaysReturnsThisTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance(SelfAlwaysReturnsThis.class, getClass()); + + @Test + public void testSelfReturnsThis() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withCast() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return (Builder) this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withTwoStatementCast() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " // sometimes people write comments here :-)", + " Builder self = (Builder) this;", + " return self;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withImplComment() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " // this is an impl comment", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsThis_withInlineComment() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return /* self */ this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelfReturnsNewBuilder() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return new Builder();", + " }", + "}") + .addOutputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testSelf_voidReturn() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public void self() {", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_differentReturnType() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public String self() {", + " return \"hi\";", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_static() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public static Builder self() {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_notNamedSelf() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder selfie() {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_hasParams() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self(int foo) {", + " return new Builder();", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testSelf_abstract() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public abstract class Builder {", + " public abstract Builder self();", + "}") + .expectUnchanged() + .doTest(); + } + + // TODO(kak): add a test for the inheritance style Builder (which requires a (T) cast). +} diff --git a/docs/bugpattern/SelfAlwaysReturnsThis.md b/docs/bugpattern/SelfAlwaysReturnsThis.md new file mode 100644 index 00000000000..044f768d0f3 --- /dev/null +++ b/docs/bugpattern/SelfAlwaysReturnsThis.md @@ -0,0 +1,29 @@ +A common pattern for abstract `Builders` is to declare an instance method named +`self()`, which subtypes override and implement as `return this` (see Effective +Java 3rd Edition, Item 2). + +Returning anything other than `this` from an instance method named `self()` with +a return type that matches the enclosing class will be confusing for readers and +callers. + +## Casting + +If an unchecked cast is required, use a single-statement cast, with the +suppression on the method (rather than the statement). For example + +```java + @SuppressWarnings("unchecked") + default U self() { + return (U) this; + } +``` + +Instead of: + +```java + default U self() { + @SuppressWarnings("unchecked") + U self = (U) this; + return self; + } +``` From e5c03b47f46f8a54455c71e2556d64b3876a2bd0 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 7 Jun 2022 11:57:53 -0700 Subject: [PATCH 015/102] Mark methods that `return self()` as `@CanIgnoreReturnValue`. #checkreturnvalue PiperOrigin-RevId: 453491721 --- .../CanIgnoreReturnValueSuggester.java | 13 ++++++++ .../CanIgnoreReturnValueSuggesterTest.java | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java index 7bc9ab31982..f89a6169a0b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -34,6 +34,7 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.ReturnTree; @@ -90,6 +91,12 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { return Description.NO_MATCH; } + if (methodSymbol.getSimpleName().contentEquals("self") + && methodSymbol.getParameters().isEmpty()) { + // would we want to actually suggest @CheckReturnValue on a self() method??? + return Description.NO_MATCH; + } + // if the method is already directly annotated w/ @CIRV, bail out if (hasAnnotation(methodTree, CIRV, state)) { return Description.NO_MATCH; @@ -135,6 +142,12 @@ private boolean returnsThis(ReturnTree returnTree) { return true; } } + if (returnExpression instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) returnExpression; + if (state.getSourceForNode(mit.getMethodSelect()).contentEquals("self")) { + return true; + } + } return false; } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java index 64e9b687a70..f99cae79e89 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -52,6 +52,36 @@ public void testSimpleCase() { .doTest(); } + @Test + public void testReturnSelf_b234875737() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getValue() {", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .doTest(); + } + @Test public void testSimpleCaseAlreadyAnnotatedWithCirv() { helper From 51da16e7aa529ea7f37d415a995afde6c8e18bba Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 7 Jun 2022 12:08:38 -0700 Subject: [PATCH 016/102] Introduce `NullArgumentForNonNullParameter`. There is a small bit of overlap between what this check finds and what `NullTernary` finds, but it looks like that acounts for maybe 1% of the hits for this check, so that seems tolerable. PiperOrigin-RevId: 453494285 --- .../NullArgumentForNonNullParameter.java | 217 ++++++++++++++++++ .../bugpatterns/nullness/NullnessUtils.java | 11 + .../nullness/ParameterMissingNullable.java | 17 +- .../scanner/BuiltInCheckerSuppliers.java | 2 + .../NullArgumentForNonNullParameterTest.java | 189 +++++++++++++++ 5 files changed, 423 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java new file mode 100644 index 00000000000..61a4c95f182 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java @@ -0,0 +1,217 @@ +/* + * 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.nullness; + +import static com.google.common.collect.Streams.forEachPair; +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.VisitorState.memoize; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasDefinitelyNullBranch; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.hasExtraParameterForEnclosingInstance; +import static com.google.errorprone.bugpatterns.nullness.NullnessUtils.nullnessChecksShouldBeConservative; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.suppliers.Suppliers.typeFromString; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +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.MethodInvocationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; +import com.google.errorprone.dataflow.nullnesspropagation.Nullness; +import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnnotations; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.suppliers.Supplier; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.util.Name; +import java.util.List; +import org.jspecify.nullness.NullMarked; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern(summary = "Null is not permitted for this parameter.", severity = ERROR) +public final class NullArgumentForNonNullParameter extends BugChecker + implements MethodInvocationTreeMatcher, NewClassTreeMatcher { + private static final Supplier JAVA_OPTIONAL_TYPE = typeFromString("java.util.Optional"); + private static final Supplier GUAVA_OPTIONAL_TYPE = + typeFromString("com.google.common.base.Optional"); + private static final Supplier OF_NAME = memoize(state -> state.getName("of")); + private static final Supplier COM_GOOGLE_COMMON_PREFIX_NAME = + memoize(state -> state.getName("com.google.common.")); + + private final boolean beingConservative; + + public NullArgumentForNonNullParameter(ErrorProneFlags flags) { + this.beingConservative = nullnessChecksShouldBeConservative(flags); + } + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return match(getSymbol(tree), tree.getArguments(), state); + } + + @Override + public Description matchNewClass(NewClassTree tree, VisitorState state) { + return match(getSymbol(tree), tree.getArguments(), state); + } + + private Description match( + MethodSymbol methodSymbol, List args, VisitorState state) { + if (state.errorProneOptions().isTestOnlyTarget()) { + return NO_MATCH; // The tests of `foo` often invoke `foo(null)` to verify that it NPEs. + } + + if (hasExtraParameterForEnclosingInstance(methodSymbol)) { + // TODO(b/232103314): Figure out the right way to handle the implicit outer `this` parameter. + return NO_MATCH; + } + + if (methodSymbol.isVarArgs()) { + /* + * TODO(b/232103314): Figure out the right way to handle this, or at least handle all + * parameters but the last. + */ + return NO_MATCH; + } + + forEachPair( + args.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 (!argumentMustBeNonNull(paramSymbol, state)) { + return; + } + + state.reportMatch(describeMatch(argTree)); + }); + + return NO_MATCH; // Any matches were reported through state.reportMatch. + } + + private boolean argumentMustBeNonNull(VarSymbol sym, VisitorState state) { + if (sym.asType().isPrimitive()) { + return true; + } + + /* + * Though we get most of our nullness information from annotations, there are technical + * obstacles to relying purely on them, including around type variables (see comments below)—not + * to mention that there are no annotations on JDK classes. + * + * As a workaround, we can hardcode specific APIs that feel worth the effort. For now, the only + * ones we hardcode are the two Optional.of methods. Those just happen to be the ones that I + * thought of and found hits for in our codebase. + */ + if (sym.owner.name.equals(OF_NAME.get(state)) + && (isParameterOfMethodOnType(sym, JAVA_OPTIONAL_TYPE, state) + || isParameterOfMethodOnType(sym, GUAVA_OPTIONAL_TYPE, state))) { + return true; + } + + /* + * TODO(b/203207989): In theory, we should program this check to exclude inner classes until we + * fix the bug in MoreAnnotations.getDeclarationAndTypeAttributes, which is used by + * fromAnnotationsOn. In practice, annotations on both inner classes and outer classes are rare + * (especially when NullableOnContainingClass is enabled!), so this code currently still looks + * at parameters that are inner types, even though we might misinterpret them. + */ + Nullness nullness = NullnessAnnotations.fromAnnotationsOn(sym).orElse(null); + + if (nullness == Nullness.NONNULL && !beingConservative) { + /* + * Much code in the wild has @NonNull annotations on parameters that are apparently + * legitimately passed null arguments. Thus, we don't trust such annotations when running in + * conservative mode. + * + * TODO(cpovirk): Instead of ignoring @NonNull annotations entirely, add special cases for + * specific badly annotated APIs. Or better yet, get the annotations fixed. + */ + return true; + } + if (nullness == Nullness.NULLABLE) { + return false; + } + + if (sym.asType().getKind() == TYPEVAR) { + /* + * TODO(cpovirk): We could sometimes return true if we looked for @NullMarked and for any + * annotations on the type-parameter bounds. But looking at type-parameter bounds is hard + * because of JDK-8225377. + */ + return false; + } + + if (enclosingAnnotationDefaultsNonTypeVariablesToNonNull(sym, state)) { + return true; + } + + return false; + } + + private static boolean isParameterOfMethodOnType( + VarSymbol sym, Supplier typeSupplier, VisitorState state) { + Type target = typeSupplier.get(state); + return target != null && state.getTypes().isSameType(sym.enclClass().type, target); + } + + private boolean enclosingAnnotationDefaultsNonTypeVariablesToNonNull( + Symbol sym, VisitorState state) { + for (; sym != null; sym = sym.getEnclosingElement()) { + if (hasAnnotation(sym, "com.google.protobuf.Internal$ProtoNonnullApi", state)) { + return true; + } + /* + * Similar to @NonNull (discussed above), the "default to non-null" annotation @NullMarked is + * sometimes used on code that hasn't had @Nullable annotations added to it where necessary. + * To avoid false positives, our conservative mode trusts @NullMarked only when it appears in + * com.google.common. + * + * TODO(cpovirk): Expand the list of packages that our conservative mode trusts @NullMarked + * on. We might be able to identify some packages that would be safe to trust today. For + * others, we could use ParameterMissingNullable, which adds missing annotations in situations + * similar to the ones identified by this check. (But note that ParameterMissingNullable + * doesn't help with calls that cross file boundaries.) + */ + if (hasAnnotation(sym, NullMarked.class, state) + && (!beingConservative + || sym.packge().fullname.startsWith(COM_GOOGLE_COMMON_PREFIX_NAME.get(state)))) { + return true; + } + } + return false; + } +} 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 6dff9193333..1eee4bb056f 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 @@ -24,6 +24,7 @@ import static com.google.errorprone.fixes.SuggestedFix.emptyFix; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.suppliers.Suppliers.JAVA_LANG_VOID_TYPE; +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.hasAnnotation; @@ -67,6 +68,7 @@ import com.sun.tools.javac.code.Kinds.KindSelector; 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.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.processing.JavacProcessingEnvironment; @@ -231,6 +233,15 @@ static boolean isAlreadyAnnotatedNullable(Symbol symbol) { return NullnessAnnotations.fromAnnotationsOn(symbol).orElse(null) == Nullness.NULLABLE; } + static boolean hasExtraParameterForEnclosingInstance(MethodSymbol symbol) { + // TODO(b/232103314): Figure out which cases the implicit outer `this` parameter exists in. + if (!symbol.isConstructor()) { + return false; + } + ClassSymbol constructedClass = enclosingClass(symbol); + return enclosingClass(constructedClass) != null && !constructedClass.isStatic(); + } + @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 ced1b26104b..33c5fb95f97 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 @@ -22,10 +22,10 @@ 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.hasExtraParameterForEnclosingInstance; 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; @@ -55,7 +55,6 @@ 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; @@ -241,25 +240,17 @@ 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. + // TODO(b/232103314): 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. + * TODO(b/232103314): Figure out the right way to handle this, or at least handle all + * parameters but the last. */ return 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 c2c98b69100..d43703910ca 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -495,6 +495,7 @@ import com.google.errorprone.bugpatterns.nullness.EqualsMissingNullable; import com.google.errorprone.bugpatterns.nullness.ExtendsObject; import com.google.errorprone.bugpatterns.nullness.FieldMissingNullable; +import com.google.errorprone.bugpatterns.nullness.NullArgumentForNonNullParameter; import com.google.errorprone.bugpatterns.nullness.ParameterMissingNullable; import com.google.errorprone.bugpatterns.nullness.ReturnMissingNullable; import com.google.errorprone.bugpatterns.nullness.UnnecessaryCheckNotNull; @@ -707,6 +708,7 @@ public static ScannerSupplier errorChecks() { NonCanonicalStaticImport.class, NonFinalCompileTimeConstant.class, NonRuntimeAnnotation.class, + NullArgumentForNonNullParameter.class, NullTernary.class, NullableOnContainingClass.class, OptionalEquality.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java new file mode 100644 index 00000000000..68868d68796 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java @@ -0,0 +1,189 @@ +/* + * 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.nullness; + +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 NullArgumentForNonNullParameter}Test */ +@RunWith(JUnit4.class) +public class NullArgumentForNonNullParameterTest { + private final CompilationTestHelper conservativeHelper = + CompilationTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()); + private final CompilationTestHelper aggressiveHelper = + CompilationTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + private final BugCheckerRefactoringTestHelper aggressiveRefactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(NullArgumentForNonNullParameter.class, getClass()) + .setArgs("-XepOpt:Nullness:Conservative=false"); + + @Test + public void testPositivePrimitive() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import java.util.Optional;", + "class Foo {", + " void consume(int i) {}", + " void foo(Optional o) {", + " // BUG: Diagnostic contains: ", + " consume(o.orElse(null));", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveAnnotatedNonnullAggressive() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import javax.annotation.Nonnull;", + "class Foo {", + " void consume(@Nonnull String s) {}", + " void foo() {", + " // BUG: Diagnostic contains: ", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeAnnotatedNonnullConservative() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import javax.annotation.Nonnull;", + "class Foo {", + " void consume(@Nonnull String s) {}", + " void foo() {", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveJavaOptionalOf() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import java.util.Optional;", + "class Foo {", + " void foo() {", + " // BUG: Diagnostic contains: ", + " Optional.of(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveGuavaOptionalOf() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import com.google.common.base.Optional;", + "class Foo {", + " void foo() {", + " // BUG: Diagnostic contains: ", + " Optional.of(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveNullMarkedComGoogleCommon() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import com.google.common.base.Ascii;", + "class Foo {", + " void foo() {", + " // BUG: Diagnostic contains: ", + " Ascii.toLowerCase(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedComGoogleCommonButNullable() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import com.google.common.collect.ImmutableSet;", + "class Foo {", + " void foo() {", + " ImmutableSet.of().contains(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testPositiveNullMarkedOtherPackageAggressive() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "class Foo {", + " void consume(String s) {}", + " void foo() {", + " // BUG: Diagnostic contains: ", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedNonComGoogleCommonPackageConservative() { + conservativeHelper + .addSourceLines( + "Foo.java", + "import org.jspecify.nullness.NullMarked;", + "@NullMarked", + "class Foo {", + " void consume(String s) {}", + " void foo() {", + " consume(null);", + " }", + "}") + .doTest(); + } + + @Test + public void testNegativeNullMarkedTypeVariable() { + aggressiveHelper + .addSourceLines( + "Foo.java", + "import com.google.common.collect.ImmutableSet;", + "class Foo {", + " void foo() {", + " ImmutableSet.of(null);", + " }", + "}") + .doTest(); + } +} From b349e812051a54ec54894dae9579897632759211 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 8 Jun 2022 03:14:41 -0700 Subject: [PATCH 017/102] Simplify some logic in SelfAlwaysReturnsThis. PiperOrigin-RevId: 453639765 --- .../bugpatterns/SelfAlwaysReturnsThis.java | 68 +++++++++---------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java index 3ac60a270a9..ccc45d981e6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java @@ -16,7 +16,9 @@ package com.google.errorprone.bugpatterns; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +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; @@ -36,8 +38,8 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.VariableTree; +import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Symbol.VarSymbol; /** * Non-abstract instance methods named {@code self()} that return the enclosing class must always @@ -49,9 +51,6 @@ + " 'return this'", severity = WARNING) public final class SelfAlwaysReturnsThis extends BugChecker implements MethodTreeMatcher { - - // TODO(kak): can we use a MethodMatcher instead? Or is that only for MethodInvocationTree's? - @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { MethodSymbol methodSymbol = getSymbol(methodTree); @@ -67,18 +66,18 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { || !methodSymbol.getParameters().isEmpty() || methodSymbol.isStatic() || methodTree.getBody() == null) { - return Description.NO_MATCH; + return NO_MATCH; } // * not have a void (or Void) return type Tree returnType = methodTree.getReturnType(); if (isVoidType(getType(returnType), state)) { - return Description.NO_MATCH; + return NO_MATCH; } // * have the same return type as the enclosing type if (!isSameType(getType(returnType), enclosingClass(methodSymbol).type, state)) { - return Description.NO_MATCH; + return NO_MATCH; } // * have a body that is exactly 1 statement @@ -89,43 +88,22 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { if (statement instanceof ReturnTree) { ExpressionTree returnExpression = ((ReturnTree) statement).getExpression(); - // e.g., `return this;` - if (isThis(returnExpression)) { - return Description.NO_MATCH; - } - - // e.g., `return (T) this;` - if (returnExpression instanceof TypeCastTree) { - TypeCastTree typeCastTree = (TypeCastTree) returnExpression; - if (isThis(typeCastTree.getExpression())) { - return Description.NO_MATCH; - } + if (maybeCastThis(returnExpression)) { + return NO_MATCH; } } } // * or have a body that is exactly 2 statement if (methodTree.getBody().getStatements().size() == 2) { - // * the 1st statement is an assignment (e.g., Builder self = (Builder) this;) StatementTree firstStatement = methodTree.getBody().getStatements().get(0); - if (firstStatement instanceof VariableTree) { - VariableTree variableTree = (VariableTree) firstStatement; - if (variableTree.getInitializer() instanceof TypeCastTree) { - TypeCastTree typeCastTree = (TypeCastTree) variableTree.getInitializer(); - if (isThis(typeCastTree.getExpression())) { - VarSymbol assignedVariable = getSymbol(variableTree); - - // * the 2nd statement is a return of the previous variable (e.g., return self;) - StatementTree secondStatement = methodTree.getBody().getStatements().get(1); - if (secondStatement instanceof ReturnTree) { - ReturnTree returnTree = (ReturnTree) secondStatement; - if (assignedVariable != null - && assignedVariable.equals(getSymbol(returnTree.getExpression()))) { - return Description.NO_MATCH; - } - } - } + StatementTree secondStatement = methodTree.getBody().getStatements().get(1); + if (firstStatement instanceof VariableTree && secondStatement instanceof ReturnTree) { + if (maybeCastThis(((VariableTree) firstStatement).getInitializer()) + && getSymbol(firstStatement) + .equals(getSymbol(((ReturnTree) secondStatement).getExpression()))) { + return NO_MATCH; } } } @@ -134,7 +112,23 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { methodTree, SuggestedFix.replace(methodTree.getBody(), "{ return this; }")); } - /** Returns whether or not the given {@link ExpressionTree} is exactly {@code this}. */ + private static boolean maybeCastThis(Tree tree) { + return firstNonNull( + new SimpleTreeVisitor() { + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return visit(tree.getExpression(), null); + } + + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return isThis(tree); + } + }.visit(tree, null), + false); + } + + /** Returns whether the given {@link ExpressionTree} is exactly {@code this}. */ private static boolean isThis(ExpressionTree expression) { if (expression instanceof IdentifierTree) { return ((IdentifierTree) expression).getName().contentEquals("this"); From d88561fe15d00ca4f7fcad9232ea8b2c1a1cda23 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 8 Jun 2022 09:29:49 -0700 Subject: [PATCH 018/102] Refer to @NullMarked by name instead of by class literal. The open-source Error Prone release does not yet depend on the JSpecify annotations, and there's no reason for us to build in any kind of dependency on the class. (I'm not sure how strong a dependency "NullMarked.class" creates, but we might as well avoid it when we can.) followup to https://github.com/google/error-prone/commit/51da16e7aa529ea7f37d415a995afde6c8e18bba PiperOrigin-RevId: 453698019 --- .../nullness/NullArgumentForNonNullParameter.java | 3 +-- .../NullArgumentForNonNullParameterTest.java | 15 --------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java index 61a4c95f182..e1a42cfac25 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java @@ -48,7 +48,6 @@ import com.sun.tools.javac.code.Type; import com.sun.tools.javac.util.Name; import java.util.List; -import org.jspecify.nullness.NullMarked; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern(summary = "Null is not permitted for this parameter.", severity = ERROR) @@ -206,7 +205,7 @@ private boolean enclosingAnnotationDefaultsNonTypeVariablesToNonNull( * similar to the ones identified by this check. (But note that ParameterMissingNullable * doesn't help with calls that cross file boundaries.) */ - if (hasAnnotation(sym, NullMarked.class, state) + if (hasAnnotation(sym, "org.jspecify.nullness.NullMarked", state) && (!beingConservative || sym.packge().fullname.startsWith(COM_GOOGLE_COMMON_PREFIX_NAME.get(state)))) { return true; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java index 68868d68796..1f908948e4f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java @@ -111,21 +111,6 @@ public void testPositiveGuavaOptionalOf() { .doTest(); } - @Test - public void testPositiveNullMarkedComGoogleCommon() { - conservativeHelper - .addSourceLines( - "Foo.java", - "import com.google.common.base.Ascii;", - "class Foo {", - " void foo() {", - " // BUG: Diagnostic contains: ", - " Ascii.toLowerCase(null);", - " }", - "}") - .doTest(); - } - @Test public void testNegativeNullMarkedComGoogleCommonButNullable() { conservativeHelper From f13e6eb59c68d48d42749676c7d26f2dc8ff9888 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Wed, 8 Jun 2022 10:00:39 -0700 Subject: [PATCH 019/102] Add tests for complex implementations of `self()`. PiperOrigin-RevId: 453704867 --- .../bugpatterns/SelfAlwaysReturnsThis.java | 2 + .../SelfAlwaysReturnsThisTest.java | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java index ccc45d981e6..56b609ac0a6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java @@ -80,6 +80,8 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { return NO_MATCH; } + // TODO(kak): we should probably re-used the TreePathScanner from CanIgnoreReturnValueSuggester + // * have a body that is exactly 1 statement if (methodTree.getBody().getStatements().size() == 1) { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java index 9b338b30418..46256fc80f6 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java @@ -58,6 +58,61 @@ public void testSelfReturnsThis_withCast() { .doTest(); } + @Test + public void testSelfReturnsThis_withCastAndTryCatch() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " try {", + " return (Builder) this;", + " } catch (ClassCastException e) {", + // sometimes people log here? + " throw e;", + " }", + " }", + "}") + // TODO(b/235255949): this should probably be .expectUnchanged() + .addOutputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .doTest(); + } + + @Test + public void testSelfReturnsThis_withMultipleReturnStatements() { + helper + .addInputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " if (System.currentTimeMillis() % 2 == 0) {", + " return this;", + " } else {", + " return this;", + " }", + " }", + "}") + // TODO(b/235255949): this should probably be .expectUnchanged() + .addOutputLines( + "Builder.java", + "package com.google.frobber;", + "public final class Builder {", + " public Builder self() {", + " return this;", + " }", + "}") + .doTest(); + } + @Test public void testSelfReturnsThis_withTwoStatementCast() { helper From f7b5b9a0b6a2a889dc57fbc21b29cfc3874ae78c Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 9 Jun 2022 02:48:38 -0700 Subject: [PATCH 020/102] CharSequence is better typed as a String, if possible. String offers more guarantees, such as immutability. PiperOrigin-RevId: 453875723 --- .../bugpatterns/PreferredInterfaceType.java | 3 ++- .../bugpatterns/PreferredInterfaceTypeTest.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java b/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java index ba99856149a..0307c41db99 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/PreferredInterfaceType.java @@ -110,7 +110,8 @@ public final class PreferredInterfaceType extends BugChecker implements Compilat "com.google.common.collect.ImmutableSetMultimap", "com.google.common.collect.ImmutableMultimap", "com.google.common.collect.ListMultimap", - "com.google.common.collect.SetMultimap")); + "com.google.common.collect.SetMultimap"), + BetterTypes.of(isDescendantOf("java.lang.CharSequence"), "java.lang.String")); private static final Matcher INTERESTING_TYPE = anyOf( diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java index 75ce5ddf2e5..f4fdf93dc59 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/PreferredInterfaceTypeTest.java @@ -869,4 +869,16 @@ public void replacementNotSubtypeOfDeclaredType_noFinding() { "}") .doTest(); } + + @Test + public void charSequences() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " // BUG: Diagnostic contains: String", + " private final CharSequence a = \"foo\";", + "}") + .doTest(); + } } From f464a420188e45a08b49edafe1ba1d233fef2f31 Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Thu, 9 Jun 2022 07:46:35 -0700 Subject: [PATCH 021/102] Documentation-only enhancement for StaticProtoFuzzer. Gives an example of a static builder method. * Separates documentation out into its own .md file PiperOrigin-RevId: 453923400 --- docs/bugpattern/StaticProtoFuzzer.md | 63 ++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/bugpattern/StaticProtoFuzzer.md diff --git a/docs/bugpattern/StaticProtoFuzzer.md b/docs/bugpattern/StaticProtoFuzzer.md new file mode 100644 index 00000000000..a38781d349d --- /dev/null +++ b/docs/bugpattern/StaticProtoFuzzer.md @@ -0,0 +1,63 @@ +ProtoFuzzer is a mutable class, even when seeded by a compile-time constant. +We're trying to avoid the following pitfalls which can arise when assigning a +ProtoFuzzer to a static field: + +* Accidental state leakage between unit tests, causing non-deterministic or + flaky behavior. In this scenario, it's recommended to instantiate a distinct + ProtoFuzzer for each relevant unit test, possibly by using a `@Before` + method. + +``` {.good} + private ProtoFuzzer protoFuzzer; + ... + @Before + public void setUp() { + ... + // Customize as appropriate + protoFuzzer = ProtoFuzzer.newBuilder().setSeed(...).build(); + ... + } +``` + +* If a static ProtoFuzzer is used to initialize other static fields, then this + initialization process can have program-order dependency; for example, + re-ordering two such initialized fields can cause their values to change. + This problem can be avoided by using static builder methods to initialize. + +``` {.bad} +private static final ProtoFuzzer protoFuzzer = + ProtoFuzzer.newBuilder() + .setSeed(...) + .build(); +... +// Re-ordering myFirstCustomProto and mySecondCustomProto can change their values! +private static final MyCustomProto myFirstCustomProto = + protoFuzzer.makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +private static final MyCustomProto mySecondCustomProto = + protoFuzzer.makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); + +``` + +Instead, create a static builder method and replace references to the static +ProtoFuzzer field with calls to the builder: + +``` {.good} + +private static final MyCustomProto myFirstCustomProto = + buildMyCustomProtoFuzzer().makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +private static final MyCustomProto mySecondCustomProto = + buildMyCustomProtoFuzzer().makeMessageOfType( + MyCustomProto.getDefaultInstance() + ); +... +private static ProtoFuzzer buildMyCustomProtoFuzzer() { + // Customize as appropriate + return ProtoFuzzer.newBuilder().setSeed(...).build(); +} +``` From 4dd66d468b37252b5e38dba279813c7058985d68 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 13 Jun 2022 11:48:25 -0700 Subject: [PATCH 022/102] Enable fixes for GuardedBy bugs PiperOrigin-RevId: 454663380 --- .../threadsafety/GuardedByChecker.java | 15 +---------- .../threadsafety/HeldLockAnalyzer.java | 25 ++++--------------- .../threadsafety/GuardedByCheckerTest.java | 6 ----- .../threadsafety/HeldLockAnalyzerTest.java | 5 ---- 4 files changed, 6 insertions(+), 45 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 55900c9057d..31cd39267d0 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 @@ -21,7 +21,6 @@ import com.google.common.base.Joiner; 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.LambdaExpressionTreeMatcher; @@ -55,16 +54,6 @@ 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 public Description matchMethod(MethodTree tree, VisitorState state) { // Constructors (and field initializers, instance initializers, and class initializers) are free @@ -89,9 +78,7 @@ private void analyze(VisitorState state) { (ExpressionTree tree, GuardedByExpression guard, HeldLockSet live) -> report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state), tree1 -> isSuppressed(tree1, state), - flags, - reportMissingGuards, - checkTryWithResources); + flags); } @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 1daff1c5725..28dd4bdcd16 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 @@ -85,14 +85,10 @@ public static void analyze( VisitorState state, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { HeldLockSet locks = HeldLockSet.empty(); locks = handleMonitorGuards(state, locks, flags); - new LockScanner( - state, listener, isSuppressed, flags, reportMissingGuards, checkTryWithResources) - .scan(state.getPath(), locks); + new LockScanner(state, listener, isSuppressed, flags).scan(state.getPath(), locks); } // Don't use Class#getName() for inner classes, we don't want `Monitor$Guard` @@ -127,8 +123,6 @@ private static class LockScanner extends TreePathScanner { private final LockEventListener listener; 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(); @@ -136,15 +130,11 @@ private LockScanner( VisitorState visitorState, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { this.visitorState = visitorState; this.listener = listener; this.isSuppressed = isSuppressed; this.flags = flags; - this.reportMissingGuards = reportMissingGuards; - this.checkTryWithResources = checkTryWithResources; } @Override @@ -187,12 +177,9 @@ 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); - // 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)); - } + scan(tree.getBlock(), locks.plusAll(releasedLocks)); scan(tree.getCatches(), locks.plusAll(releasedLocks)); scan(tree.getFinallyBlock(), locks); return null; @@ -254,9 +241,7 @@ private void checkMatch(ExpressionTree tree, HeldLockSet locks) { GuardedBySymbolResolver.from(tree, visitorState.withPath(getCurrentPath())), flags); if (!guard.isPresent()) { - if (reportMissingGuards) { - invalidLock(tree, locks, guardString); - } + invalidLock(tree, locks, guardString); continue; } Optional boundGuard = 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 16a977fc19a..1eb246cfabe 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 @@ -1673,7 +1673,6 @@ public void testStaticMemberClass_enclosingInstanceLock() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1770,7 +1769,6 @@ public void testMissingGuard() { " lib.doSomething();", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1818,7 +1816,6 @@ public void parameterGuard() { " worker.f(work);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1846,7 +1843,6 @@ public void parameterGuardNegative() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1873,7 +1869,6 @@ public void parameterGuardNegativeSimpleName() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1896,7 +1891,6 @@ public void varargsArity() { " f(0);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java index 4eb7d33dd62..710c7e164ae 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java @@ -20,7 +20,6 @@ import com.google.errorprone.BugPattern; import com.google.errorprone.CompilationTestHelper; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.matchers.Description; import com.sun.source.tree.Tree; @@ -247,10 +246,6 @@ public void testLockMethodEnclosingAccess() { @BugPattern(name = "GuardedByLockSet", summary = "", explanation = "", severity = ERROR) public static class GuardedByLockSetAnalyzer extends GuardedByChecker { - public GuardedByLockSetAnalyzer(ErrorProneFlags errorProneFlags) { - super(errorProneFlags); - } - @Override protected Description checkGuardedAccess( Tree tree, GuardedByExpression guard, HeldLockSet live, VisitorState state) { From 6e8748e5a6aa923ccb49570704a083c76459cd8e Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Mon, 13 Jun 2022 16:40:20 -0700 Subject: [PATCH 023/102] Automated rollback of commit 4dd66d468b37252b5e38dba279813c7058985d68. *** Reason for rollback *** Breaking lots of tests: [] *** Original change description *** Enable fixes for GuardedBy bugs *** PiperOrigin-RevId: 454727068 --- .../threadsafety/GuardedByChecker.java | 15 ++++++++++- .../threadsafety/HeldLockAnalyzer.java | 25 +++++++++++++++---- .../threadsafety/GuardedByCheckerTest.java | 6 +++++ .../threadsafety/HeldLockAnalyzerTest.java | 5 ++++ 4 files changed, 45 insertions(+), 6 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 31cd39267d0..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 @@ -21,6 +21,7 @@ import com.google.common.base.Joiner; 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.LambdaExpressionTreeMatcher; @@ -54,6 +55,16 @@ 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 public Description matchMethod(MethodTree tree, VisitorState state) { // Constructors (and field initializers, instance initializers, and class initializers) are free @@ -78,7 +89,9 @@ private void analyze(VisitorState state) { (ExpressionTree tree, GuardedByExpression guard, HeldLockSet live) -> report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state), tree1 -> isSuppressed(tree1, state), - flags); + flags, + 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 28dd4bdcd16..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 @@ -85,10 +85,14 @@ public static void analyze( VisitorState state, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags) { + GuardedByFlags flags, + boolean reportMissingGuards, + boolean checkTryWithResources) { HeldLockSet locks = HeldLockSet.empty(); locks = handleMonitorGuards(state, locks, flags); - new LockScanner(state, listener, isSuppressed, flags).scan(state.getPath(), locks); + new LockScanner( + state, listener, isSuppressed, flags, reportMissingGuards, checkTryWithResources) + .scan(state.getPath(), locks); } // Don't use Class#getName() for inner classes, we don't want `Monitor$Guard` @@ -123,6 +127,8 @@ private static class LockScanner extends TreePathScanner { private final LockEventListener listener; 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(); @@ -130,11 +136,15 @@ private LockScanner( VisitorState visitorState, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags) { + GuardedByFlags flags, + boolean reportMissingGuards, + boolean checkTryWithResources) { this.visitorState = visitorState; this.listener = listener; this.isSuppressed = isSuppressed; this.flags = flags; + this.reportMissingGuards = reportMissingGuards; + this.checkTryWithResources = checkTryWithResources; } @Override @@ -177,9 +187,12 @@ 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); + // 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.getBlock(), locks.plusAll(releasedLocks)); + if (checkTryWithResources || resources.isEmpty()) { + scan(tree.getBlock(), locks.plusAll(releasedLocks)); + } scan(tree.getCatches(), locks.plusAll(releasedLocks)); scan(tree.getFinallyBlock(), locks); return null; @@ -241,7 +254,9 @@ private void checkMatch(ExpressionTree tree, HeldLockSet locks) { GuardedBySymbolResolver.from(tree, visitorState.withPath(getCurrentPath())), flags); if (!guard.isPresent()) { - invalidLock(tree, locks, guardString); + if (reportMissingGuards) { + invalidLock(tree, locks, guardString); + } continue; } Optional boundGuard = 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 1eb246cfabe..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 @@ -1673,6 +1673,7 @@ public void testStaticMemberClass_enclosingInstanceLock() { " }", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1769,6 +1770,7 @@ public void testMissingGuard() { " lib.doSomething();", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1816,6 +1818,7 @@ public void parameterGuard() { " worker.f(work);", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1843,6 +1846,7 @@ public void parameterGuardNegative() { " }", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1869,6 +1873,7 @@ public void parameterGuardNegativeSimpleName() { " }", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1891,6 +1896,7 @@ public void varargsArity() { " f(0);", " }", "}") + .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java index 710c7e164ae..4eb7d33dd62 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java @@ -20,6 +20,7 @@ import com.google.errorprone.BugPattern; import com.google.errorprone.CompilationTestHelper; +import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.matchers.Description; import com.sun.source.tree.Tree; @@ -246,6 +247,10 @@ public void testLockMethodEnclosingAccess() { @BugPattern(name = "GuardedByLockSet", summary = "", explanation = "", severity = ERROR) public static class GuardedByLockSetAnalyzer extends GuardedByChecker { + public GuardedByLockSetAnalyzer(ErrorProneFlags errorProneFlags) { + super(errorProneFlags); + } + @Override protected Description checkGuardedAccess( Tree tree, GuardedByExpression guard, HeldLockSet live, VisitorState state) { From 75e9ae6a2bebcb0b71786b28f82fcc29cb8dbb5a Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 14 Jun 2022 07:56:49 -0700 Subject: [PATCH 024/102] Update `SelfAlwaysReturnsThis` docs. PiperOrigin-RevId: 454860733 --- docs/bugpattern/SelfAlwaysReturnsThis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bugpattern/SelfAlwaysReturnsThis.md b/docs/bugpattern/SelfAlwaysReturnsThis.md index 044f768d0f3..6b589797dcc 100644 --- a/docs/bugpattern/SelfAlwaysReturnsThis.md +++ b/docs/bugpattern/SelfAlwaysReturnsThis.md @@ -8,8 +8,8 @@ callers. ## Casting -If an unchecked cast is required, use a single-statement cast, with the -suppression on the method (rather than the statement). For example +If an unchecked cast is required, prefer a single-statement cast, with the +suppression on the method (rather than the statement). For example: ```java @SuppressWarnings("unchecked") From 95f5803cd6a3d6ad0d4b3df5f2518b180e8ba3a2 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 14 Jun 2022 09:36:13 -0700 Subject: [PATCH 025/102] Remove the two-statement special case from SART. PiperOrigin-RevId: 454882415 --- .../bugpatterns/SelfAlwaysReturnsThis.java | 79 ++++++++++++------- .../SelfAlwaysReturnsThisTest.java | 20 +---- 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java index 56b609ac0a6..4da47b30279 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThis.java @@ -22,6 +22,7 @@ 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.isConsideredFinal; import static com.google.errorprone.util.ASTHelpers.isSameType; import static com.google.errorprone.util.ASTHelpers.isVoidType; @@ -32,14 +33,20 @@ import com.google.errorprone.matchers.Description; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.SimpleTreeVisitor; +import com.sun.source.util.TreePathScanner; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; /** * Non-abstract instance methods named {@code self()} that return the enclosing class must always @@ -82,32 +89,53 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { // TODO(kak): we should probably re-used the TreePathScanner from CanIgnoreReturnValueSuggester - // * have a body that is exactly 1 statement - if (methodTree.getBody().getStatements().size() == 1) { + // This TreePathScanner is mostly copied from CanIgnoreReturnValueSuggester + AtomicBoolean allReturnThis = new AtomicBoolean(true); + AtomicBoolean atLeastOneReturn = new AtomicBoolean(false); - // * the 1 statement is "return this;" or "return (T) this;" - StatementTree statement = methodTree.getBody().getStatements().get(0); - if (statement instanceof ReturnTree) { - ExpressionTree returnExpression = ((ReturnTree) statement).getExpression(); + new TreePathScanner() { + private final Set thises = new HashSet<>(); - if (maybeCastThis(returnExpression)) { - return NO_MATCH; + @Override + public Void visitVariable(VariableTree variableTree, Void unused) { + VarSymbol symbol = getSymbol(variableTree); + if (isConsideredFinal(symbol) && maybeCastThis(variableTree.getInitializer())) { + thises.add(symbol); } + return super.visitVariable(variableTree, null); } - } - // * or have a body that is exactly 2 statement - if (methodTree.getBody().getStatements().size() == 2) { - // * the 1st statement is an assignment (e.g., Builder self = (Builder) this;) - StatementTree firstStatement = methodTree.getBody().getStatements().get(0); - StatementTree secondStatement = methodTree.getBody().getStatements().get(1); - if (firstStatement instanceof VariableTree && secondStatement instanceof ReturnTree) { - if (maybeCastThis(((VariableTree) firstStatement).getInitializer()) - && getSymbol(firstStatement) - .equals(getSymbol(((ReturnTree) secondStatement).getExpression()))) { - return NO_MATCH; + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn.set(true); + if (!isThis(returnTree.getExpression())) { + allReturnThis.set(false); + // once we've set allReturnThis to false, no need to descend further + return null; } + return super.visitReturn(returnTree, null); + } + + /** Returns whether the given {@link ExpressionTree} is {@code this}. */ + private boolean isThis(ExpressionTree returnExpression) { + return maybeCastThis(returnExpression) || thises.contains(getSymbol(returnExpression)); + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; } + }.scan(state.getPath(), null); + + if (atLeastOneReturn.get() && allReturnThis.get()) { + return NO_MATCH; } return describeMatch( @@ -117,6 +145,7 @@ && getSymbol(firstStatement) private static boolean maybeCastThis(Tree tree) { return firstNonNull( new SimpleTreeVisitor() { + @Override public Boolean visitTypeCast(TypeCastTree tree, Void unused) { return visit(tree.getExpression(), null); @@ -124,17 +153,9 @@ public Boolean visitTypeCast(TypeCastTree tree, Void unused) { @Override public Boolean visitIdentifier(IdentifierTree tree, Void unused) { - return isThis(tree); + return tree.getName().contentEquals("this"); } }.visit(tree, null), false); } - - /** Returns whether the given {@link ExpressionTree} is exactly {@code this}. */ - private static boolean isThis(ExpressionTree expression) { - if (expression instanceof IdentifierTree) { - return ((IdentifierTree) expression).getName().contentEquals("this"); - } - return false; - } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java index 46256fc80f6..d7ff160b2ed 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SelfAlwaysReturnsThisTest.java @@ -74,15 +74,7 @@ public void testSelfReturnsThis_withCastAndTryCatch() { " }", " }", "}") - // TODO(b/235255949): this should probably be .expectUnchanged() - .addOutputLines( - "Builder.java", - "package com.google.frobber;", - "public final class Builder {", - " public Builder self() {", - " return this;", - " }", - "}") + .expectUnchanged() .doTest(); } @@ -101,15 +93,7 @@ public void testSelfReturnsThis_withMultipleReturnStatements() { " }", " }", "}") - // TODO(b/235255949): this should probably be .expectUnchanged() - .addOutputLines( - "Builder.java", - "package com.google.frobber;", - "public final class Builder {", - " public Builder self() {", - " return this;", - " }", - "}") + .expectUnchanged() .doTest(); } From 3f5e0c0018a988ab850ddcdcb6fe7b4658138ae4 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 15 Jun 2022 04:28:39 -0700 Subject: [PATCH 026/102] Exempt enum fields from MustBeClosed in the same way as static fields. Because enum fields are morally static. PiperOrigin-RevId: 455096852 --- .../AbstractMustBeClosedChecker.java | 11 +++++++--- .../bugpatterns/MustBeClosedCheckerTest.java | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java index 1c706eecc9f..78df7fc2be3 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractMustBeClosedChecker.java @@ -45,6 +45,7 @@ import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.UnusedReturnValueMatcher; import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.ExpressionStatementTree; @@ -245,7 +246,7 @@ private Description emptyFix(Tree tree) { private static boolean variableInitializationCountsAsClosing(VarSymbol var) { // static final fields don't need to be closed, because they never leave scope - return var.isStatic() && var.getModifiers().contains(Modifier.FINAL); + return (var.isStatic() || var.owner.isEnum()) && var.getModifiers().contains(Modifier.FINAL); } // We allow calling @MBC methods anywhere inside of a static initializer. This is a compromise: @@ -258,8 +259,12 @@ private static boolean isInStaticInitializer(VisitorState state) { return Streams.stream(state.getPath()) .anyMatch( tree -> - tree instanceof VariableTree - && variableInitializationCountsAsClosing((VarSymbol) getSymbol(tree))); + (tree instanceof VariableTree + && variableInitializationCountsAsClosing((VarSymbol) getSymbol(tree))) + || (tree instanceof AssignmentTree + && getSymbol(((AssignmentTree) tree).getVariable()) instanceof VarSymbol + && variableInitializationCountsAsClosing( + (VarSymbol) getSymbol(((AssignmentTree) tree).getVariable())))); } /** diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java index 344cf9238bf..035928d3f84 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MustBeClosedCheckerTest.java @@ -50,4 +50,26 @@ public void refactoring() { .allowBreakingChanges() // The fix is best-effort, and some variable names may clash .doTest(); } + + @Test + public void enumInitializer() { + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.errorprone.annotations.MustBeClosed;", + "import java.io.Closeable;", + "enum Test {", + " A;", + " interface Foo extends Closeable {}", + " @MustBeClosed static Foo createResource() {", + " return null;", + " }", + " private final Foo resource;", + " private final Foo resource2 = createResource();", + " Test() {", + " this.resource = createResource();", + " }", + "}") + .doTest(); + } } From 66fd5e656954690c9b4519fdc9e8ca6cc388112f Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Wed, 15 Jun 2022 11:19:12 -0700 Subject: [PATCH 027/102] Add a checker that pushes `@CanIgnoreReturnValue` annotations "down" from classes to methods/constructors. [] #checkreturnvalue PiperOrigin-RevId: 455176973 --- .../NoCanIgnoreReturnValueOnClasses.java | 247 +++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../NoCanIgnoreReturnValueOnClassesTest.java | 332 ++++++++++++++++++ 3 files changed, 581 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java new file mode 100644 index 00000000000..c37746ce382 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java @@ -0,0 +1,247 @@ +/* + * 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.MoreObjects.firstNonNull; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE; +import static com.google.errorprone.matchers.Matchers.annotations; +import static com.google.errorprone.matchers.Matchers.isType; +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.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; +import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; +import static com.google.errorprone.util.ASTHelpers.isVoidType; + +import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.MultiMatcher; +import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.SimpleTreeVisitor; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Checker that "pushes" the {@code @CanIgnoreReturnValue} annotation down from classes to methods. + */ +@BugPattern( + summary = + "@CanIgnoreReturnValue should not be applied to classes as it almost always overmatches (as" + + " it applies to constructors and all methods), and the CIRVness isn't conferred to" + + " its subclasses.", + severity = WARNING) +public final class NoCanIgnoreReturnValueOnClasses extends BugChecker implements ClassTreeMatcher { + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; + + private static final String EXTRA_SUFFIX = ""; + + @VisibleForTesting + static final String METHOD_COMMENT = " // pushed down from class to method;" + EXTRA_SUFFIX; + + @VisibleForTesting + static final String CTOR_COMMENT = " // pushed down from class to constructor;" + EXTRA_SUFFIX; + + private static final MultiMatcher HAS_CIRV_ANNOTATION = + annotations(AT_LEAST_ONE, isType(CIRV)); + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + MultiMatchResult cirvAnnotation = + HAS_CIRV_ANNOTATION.multiMatchResult(tree, state); + // if the class isn't directly annotated w/ @CIRV, bail out + if (!cirvAnnotation.matches()) { + return Description.NO_MATCH; + } + + SuggestedFix.Builder fix = SuggestedFix.builder(); + String cirvName = qualifyType(state, fix, CIRV); + + // remove @CIRV from the class + fix.delete(cirvAnnotation.onlyMatchingNode()); + + // theoretically, we could also add @CRV to the class, since all APIs will have CIRV pushed down + // onto them, but it's very likely that a larger enclosing scope will already be @CRV (otherwise + // why did the user annotate this class as @CIRV?) + + // scan the tree and add @CIRV to all non-void method declarations that aren't already annotated + // with @CIRV or @CRV + new TreePathScanner() { + @Override + public Void visitClass(ClassTree classTree, Void unused) { + // stop descending when we reach a class that's marked @CRV + return hasAnnotation(classTree, CRV, state) ? null : super.visitClass(classTree, unused); + } + + @Override + public Void visitMethod(MethodTree methodTree, Void unused) { + if (shouldAddCirv(methodTree, state)) { + String trailingComment = null; + + if (methodTree.getReturnType() == null) { // constructor + trailingComment = CTOR_COMMENT; + } else if (alwaysReturnsThis()) { + trailingComment = ""; + } else { + trailingComment = METHOD_COMMENT; + } + + fix.prefixWith(methodTree, "@" + cirvName + trailingComment + "\n"); + } + // TODO(kak): we could also consider removing CRV from individual methods (since the + // enclosing class is now annotated as CRV. + return null; + } + + private boolean alwaysReturnsThis() { + // TODO(b/236055787): share this TreePathScanner + AtomicBoolean allReturnThis = new AtomicBoolean(true); + AtomicBoolean atLeastOneReturn = new AtomicBoolean(false); + + new TreePathScanner() { + private final Set thises = new HashSet<>(); + + @Override + public Void visitVariable(VariableTree variableTree, Void unused) { + VarSymbol symbol = getSymbol(variableTree); + if (isConsideredFinal(symbol) && maybeCastThis(variableTree.getInitializer())) { + thises.add(symbol); + } + return super.visitVariable(variableTree, null); + } + + @Override + public Void visitReturn(ReturnTree returnTree, Void unused) { + atLeastOneReturn.set(true); + if (!isThis(returnTree.getExpression())) { + allReturnThis.set(false); + // once we've set allReturnThis to false, no need to descend further + return null; + } + return super.visitReturn(returnTree, null); + } + + /** Returns whether the given {@link ExpressionTree} is {@code this}. */ + private boolean isThis(ExpressionTree returnExpression) { + return maybeCastThis(returnExpression) || thises.contains(getSymbol(returnExpression)); + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + + private boolean maybeCastThis(Tree tree) { + return firstNonNull( + new SimpleTreeVisitor() { + + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return visit(tree.getExpression(), null); + } + + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return tree.getName().contentEquals("this"); + } + + @Override + public Boolean visitMethodInvocation(MethodInvocationTree tree, Void unused) { + return getSymbol(tree).getSimpleName().contentEquals("self"); + } + }.visit(tree, null), + false); + } + }.scan(getCurrentPath(), null); + + return allReturnThis.get() && atLeastOneReturn.get(); + } + + private boolean shouldAddCirv(MethodTree methodTree, VisitorState state) { + if (isVoidType(getType(methodTree.getReturnType()), state)) { // void return types + return false; + } + if (hasAnnotation(methodTree, CIRV, state)) { + return false; + } + if (hasAnnotation(methodTree, CRV, state)) { + return false; + } + // if the constructor is implicit, don't add CIRV (we can't annotate a synthetic node!) + if (isGeneratedConstructor(methodTree)) { + return false; + } + // if the method is inside an AV or AV.Builder and is abstract (no body), don't add CIRV + ClassSymbol enclosingClass = enclosingClass(getSymbol(methodTree)); + if (hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue", state) + || hasAnnotation(enclosingClass, "com.google.auto.value.AutoValue.Builder", state)) { + if (methodTree.getBody() == null) { + return false; + } + } + // TODO(kak): should we also return false for private methods? I'm betting most of them are + // "accidentally" CIRV'ed by the enclosing class; any compile errors would be caught by + // building the enclosing class anyways. + return true; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { + // don't descend into lambdas + return null; + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + // don't descend into declarations of anonymous classes + return null; + } + }.scan(state.getPath(), null); + return describeMatch(tree, fix.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 d43703910ca..e8d7010c790 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -417,6 +417,7 @@ import com.google.errorprone.bugpatterns.argumentselectiondefects.AssertEqualsArgumentOrderChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AutoValueConstructorOrderChecker; import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; +import com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses; import com.google.errorprone.bugpatterns.checkreturnvalue.UsingJsr305CheckReturnValue; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionIncompatibleType; import com.google.errorprone.bugpatterns.collectionincompatibletype.CollectionUndefinedEquality; @@ -1073,6 +1074,7 @@ public static ScannerSupplier errorChecks() { MultiVariableDeclaration.class, MultipleTopLevelClasses.class, NoAllocationChecker.class, + NoCanIgnoreReturnValueOnClasses.class, NonCanonicalStaticMemberImport.class, PackageLocation.class, ParameterComment.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java new file mode 100644 index 00000000000..71a2070ac08 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClassesTest.java @@ -0,0 +1,332 @@ +/* + * 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.NoCanIgnoreReturnValueOnClasses.CTOR_COMMENT; +import static com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses.METHOD_COMMENT; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link NoCanIgnoreReturnValueOnClasses}. */ +@RunWith(JUnit4.class) +public final class NoCanIgnoreReturnValueOnClassesTest { + + private final BugCheckerRefactoringTestHelper helper = + BugCheckerRefactoringTestHelper.newInstance( + NoCanIgnoreReturnValueOnClasses.class, getClass()); + + @Test + public void testSimpleCase_returnsThis() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_returnsSelf() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return self();", + " }", + " private Client self() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return self();", + " }", + " @CanIgnoreReturnValue", + " private Client self() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_returnsNewInstance() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return new Client();", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue" + METHOD_COMMENT, + " public Client getValue() {", + " return new Client();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testSimpleCase_explicitConstructor() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " Client() {}", + " public Client getValue() {", + " return this;", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue" + CTOR_COMMENT, + " Client() {}", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedClasses_cirvAndCrv() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "@CanIgnoreReturnValue", + "public final class Client {", + " public Client getValue() {", + " return this;", + " }", + " @CheckReturnValue", + " public static final class Nested {", + " public int getValue() {", + " return 42;", + " }", + " }", + "}") + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "import com.google.errorprone.annotations.CheckReturnValue;", + "public final class Client {", + " @CanIgnoreReturnValue", + " public Client getValue() {", + " return this;", + " }", + " @CheckReturnValue", + " public static final class Nested {", + " public int getValue() {", + " return 42;", + " }", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedClasses_bothCirv() { + helper + .addInputLines( + "User.java", + "package com.google.gaia;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public final class User {", + " public User persist() {", + " return this;", + " }", + " public static final class Builder {", + " public Builder setFirstName(String firstName) {", + " return this;", + " }", + " }", + "}") + .addOutputLines( + "User.java", + "package com.google.gaia;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class User {", + " @CanIgnoreReturnValue", + " public User persist() {", + " return this;", + " }", + " public static final class Builder {", + " @CanIgnoreReturnValue", + " public Builder setFirstName(String firstName) {", + " return this;", + " }", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testAutoValue() { + helper + .addInputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "@CanIgnoreReturnValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .addOutputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testAutoValueBuilder() { + helper + .addInputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @CanIgnoreReturnValue", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .addOutputLines( + "Animal.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@AutoValue", + "abstract class Animal {", + " abstract String name();", + " @AutoValue.Builder", + " abstract static class Builder {", + " abstract Builder setName(String value);", + " abstract Animal build();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + public void testNestedAutoValue() { + helper + .addInputLines( + "Outer.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "@CanIgnoreReturnValue", + "public class Outer {", + " public String name() {", + " return null;", + " }", + " @AutoValue", + " abstract static class Inner {", + " abstract String id();", + " }", + "}") + .addOutputLines( + "Outer.java", + "package com.google.frobber;", + "import com.google.auto.value.AutoValue;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public class Outer {", + " @CanIgnoreReturnValue" + METHOD_COMMENT, + " public String name() {", + " return null;", + " }", + " @AutoValue", + " abstract static class Inner {", + " abstract String id();", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} From 0a9db3614c479a9f4dd1e7d15909c665c451616d Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 16 Jun 2022 02:25:00 -0700 Subject: [PATCH 028/102] LongDoubleConversion: don't complain if it's a constant that doesn't _actually_ lose precision. PiperOrigin-RevId: 455330669 --- .../bugpatterns/LongDoubleConversion.java | 7 ++++- .../bugpatterns/LongDoubleConversionTest.java | 27 ++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java b/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java index 24ec06586c6..4bd71ae93fe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/LongDoubleConversion.java @@ -18,6 +18,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.constValue; import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.targetType; @@ -39,7 +40,7 @@ "Conversion from long to double may lose precision; use an explicit cast to double if this" + " was intentional", severity = WARNING) -public class LongDoubleConversion extends BugChecker implements MethodInvocationTreeMatcher { +public final class LongDoubleConversion extends BugChecker implements MethodInvocationTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { @@ -53,6 +54,10 @@ private void checkArgument(ExpressionTree argument, VisitorState state) { if (!getType(argument).getKind().equals(TypeKind.LONG)) { return; } + Object constant = constValue(argument); + if (constant instanceof Long && constant.equals((long) ((Long) constant).doubleValue())) { + return; + } ASTHelpers.TargetType targetType = targetType(state.withPath(new TreePath(state.getPath(), argument))); if (targetType != null && targetType.type().getKind().equals(TypeKind.DOUBLE)) { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java index 51e1f95d1cd..467a60d81bd 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/LongDoubleConversionTest.java @@ -33,7 +33,7 @@ public class LongDoubleConversionTest { BugCheckerRefactoringTestHelper.newInstance(LongDoubleConversion.class, getClass()); @Test - public void doesNotCompile() { + public void losesPrecision() { compilationTestHelper .addSourceLines( "Test.java", // @@ -42,6 +42,21 @@ public void doesNotCompile() { " void method(double l) {}", " {", " // BUG: Diagnostic contains:", + " method(9223372036854775806L);", + " }", + "}") + .doTest(); + } + + @Test + public void doesNotActuallyLosePrecision_noFinding() { + compilationTestHelper + .addSourceLines( + "Test.java", // + "class Test {", + " void method(Long l) {}", + " void method(double l) {}", + " {", " method(0L);", " }", "}") @@ -49,7 +64,7 @@ public void doesNotCompile() { } @Test - public void compiles() { + public void explicitlyCastToDouble_noFinding() { compilationTestHelper .addSourceLines( "Test.java", // @@ -71,7 +86,7 @@ public void refactors_simpleArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method(0L);", + " method(9223372036854775806L);", " }", "}") .addOutputLines( @@ -80,7 +95,7 @@ public void refactors_simpleArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method((double) 0L);", + " method((double) 9223372036854775806L);", " }", "}") .doTest(); @@ -95,7 +110,7 @@ public void refactors_complexArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method(0L + 1L);", + " method(9223372036854775805L + 1L);", " }", "}") .addOutputLines( @@ -104,7 +119,7 @@ public void refactors_complexArgument() { " void method(Long l) {}", " void method(double l) {}", " {", - " method((double) (0L + 1L));", + " method((double) (9223372036854775805L + 1L));", " }", "}") .doTest(); From 0477d89f14471b013115c66b15ca3e4b573013a2 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 16 Jun 2022 13:13:08 -0700 Subject: [PATCH 029/102] Enable fixes for GuardedBy bugs Roll forward of https://github.com/google/error-prone/commit/4dd66d468b37252b5e38dba279813c7058985d68 PiperOrigin-RevId: 455451529 --- .../threadsafety/GuardedByChecker.java | 15 +---------- .../threadsafety/HeldLockAnalyzer.java | 25 ++++--------------- .../threadsafety/GuardedByCheckerTest.java | 6 ----- .../threadsafety/HeldLockAnalyzerTest.java | 5 ---- 4 files changed, 6 insertions(+), 45 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 55900c9057d..31cd39267d0 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 @@ -21,7 +21,6 @@ import com.google.common.base.Joiner; 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.LambdaExpressionTreeMatcher; @@ -55,16 +54,6 @@ 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 public Description matchMethod(MethodTree tree, VisitorState state) { // Constructors (and field initializers, instance initializers, and class initializers) are free @@ -89,9 +78,7 @@ private void analyze(VisitorState state) { (ExpressionTree tree, GuardedByExpression guard, HeldLockSet live) -> report(GuardedByChecker.this.checkGuardedAccess(tree, guard, live, state), state), tree1 -> isSuppressed(tree1, state), - flags, - reportMissingGuards, - checkTryWithResources); + flags); } @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 1daff1c5725..28dd4bdcd16 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 @@ -85,14 +85,10 @@ public static void analyze( VisitorState state, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { HeldLockSet locks = HeldLockSet.empty(); locks = handleMonitorGuards(state, locks, flags); - new LockScanner( - state, listener, isSuppressed, flags, reportMissingGuards, checkTryWithResources) - .scan(state.getPath(), locks); + new LockScanner(state, listener, isSuppressed, flags).scan(state.getPath(), locks); } // Don't use Class#getName() for inner classes, we don't want `Monitor$Guard` @@ -127,8 +123,6 @@ private static class LockScanner extends TreePathScanner { private final LockEventListener listener; 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(); @@ -136,15 +130,11 @@ private LockScanner( VisitorState visitorState, LockEventListener listener, Predicate isSuppressed, - GuardedByFlags flags, - boolean reportMissingGuards, - boolean checkTryWithResources) { + GuardedByFlags flags) { this.visitorState = visitorState; this.listener = listener; this.isSuppressed = isSuppressed; this.flags = flags; - this.reportMissingGuards = reportMissingGuards; - this.checkTryWithResources = checkTryWithResources; } @Override @@ -187,12 +177,9 @@ 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); - // 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)); - } + scan(tree.getBlock(), locks.plusAll(releasedLocks)); scan(tree.getCatches(), locks.plusAll(releasedLocks)); scan(tree.getFinallyBlock(), locks); return null; @@ -254,9 +241,7 @@ private void checkMatch(ExpressionTree tree, HeldLockSet locks) { GuardedBySymbolResolver.from(tree, visitorState.withPath(getCurrentPath())), flags); if (!guard.isPresent()) { - if (reportMissingGuards) { - invalidLock(tree, locks, guardString); - } + invalidLock(tree, locks, guardString); continue; } Optional boundGuard = 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 16a977fc19a..1eb246cfabe 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 @@ -1673,7 +1673,6 @@ public void testStaticMemberClass_enclosingInstanceLock() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1770,7 +1769,6 @@ public void testMissingGuard() { " lib.doSomething();", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1818,7 +1816,6 @@ public void parameterGuard() { " worker.f(work);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1846,7 +1843,6 @@ public void parameterGuardNegative() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1873,7 +1869,6 @@ public void parameterGuardNegativeSimpleName() { " }", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } @@ -1896,7 +1891,6 @@ public void varargsArity() { " f(0);", " }", "}") - .setArgs("-XepOpt:GuardedByChecker:reportMissingGuards=true") .doTest(); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java index 4eb7d33dd62..710c7e164ae 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/HeldLockAnalyzerTest.java @@ -20,7 +20,6 @@ import com.google.errorprone.BugPattern; import com.google.errorprone.CompilationTestHelper; -import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.matchers.Description; import com.sun.source.tree.Tree; @@ -247,10 +246,6 @@ public void testLockMethodEnclosingAccess() { @BugPattern(name = "GuardedByLockSet", summary = "", explanation = "", severity = ERROR) public static class GuardedByLockSetAnalyzer extends GuardedByChecker { - public GuardedByLockSetAnalyzer(ErrorProneFlags errorProneFlags) { - super(errorProneFlags); - } - @Override protected Description checkGuardedAccess( Tree tree, GuardedByExpression guard, HeldLockSet live, VisitorState state) { From 42f787da7804f45057e1e136461b6812833b9c21 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Thu, 16 Jun 2022 14:00:21 -0700 Subject: [PATCH 030/102] Make `NoCanIgnoreReturnValueOnClasses` an `ERROR`, but reduce it to a `WARNING` until the cleanup is complete. #checkreturnvalue PiperOrigin-RevId: 455461545 --- .../checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java | 4 ++-- .../google/errorprone/scanner/BuiltInCheckerSuppliers.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java index c37746ce382..cfbd907270d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/NoCanIgnoreReturnValueOnClasses.java @@ -17,7 +17,7 @@ package com.google.errorprone.bugpatterns.checkreturnvalue; import static com.google.common.base.MoreObjects.firstNonNull; -import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE; import static com.google.errorprone.matchers.Matchers.annotations; @@ -67,7 +67,7 @@ "@CanIgnoreReturnValue should not be applied to classes as it almost always overmatches (as" + " it applies to constructors and all methods), and the CIRVness isn't conferred to" + " its subclasses.", - severity = WARNING) + severity = ERROR) public final class NoCanIgnoreReturnValueOnClasses extends BugChecker implements ClassTreeMatcher { private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; private static final String CIRV = "com.google.errorprone.annotations.CanIgnoreReturnValue"; 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 e8d7010c790..b123371ba9c 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -706,6 +706,7 @@ public static ScannerSupplier errorChecks() { MoreThanOneScopeAnnotationOnClass.class, MustBeClosedChecker.class, NCopiesOfChar.class, + NoCanIgnoreReturnValueOnClasses.class, NonCanonicalStaticImport.class, NonFinalCompileTimeConstant.class, NonRuntimeAnnotation.class, @@ -1074,7 +1075,6 @@ public static ScannerSupplier errorChecks() { MultiVariableDeclaration.class, MultipleTopLevelClasses.class, NoAllocationChecker.class, - NoCanIgnoreReturnValueOnClasses.class, NonCanonicalStaticMemberImport.class, PackageLocation.class, ParameterComment.class, From 089c911869b98b4c647bc8ff92dd8e1a5f4d0315 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 16 Jun 2022 15:02:37 -0700 Subject: [PATCH 031/102] Prototype enforcement that instance methods on builder either return 'this' or are annotated with `@CRV` PiperOrigin-RevId: 455474934 --- .../checkreturnvalue/BuilderReturnThis.java | 150 ++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../BuilderReturnThisTest.java | 99 ++++++++++++ docs/bugpattern/BuilderReturnThis.md | 34 ++++ 4 files changed, 285 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java create mode 100644 docs/bugpattern/BuilderReturnThis.md diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java new file mode 100644 index 00000000000..36d177867b5 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThis.java @@ -0,0 +1,150 @@ +/* + * 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.MoreObjects.firstNonNull; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.isSameType; +import static com.google.errorprone.util.ASTHelpers.isSubtype; +import static java.lang.Boolean.TRUE; + +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; + +/** Discourages builder instance methods that do not return 'this'. */ +@BugPattern(summary = "Builder instance method does not return 'this'", severity = WARNING) +public class BuilderReturnThis extends BugChecker implements MethodTreeMatcher { + + private static final String CRV = "com.google.errorprone.annotations.CheckReturnValue"; + + @Override + public Description matchMethod(MethodTree tree, VisitorState state) { + MethodSymbol sym = getSymbol(tree); + if (tree.getBody() == null) { + return NO_MATCH; + } + if (!instanceReturnsBuilder(sym, state)) { + return NO_MATCH; + } + if (!nonThisReturns(tree, state)) { + return NO_MATCH; + } + SuggestedFix.Builder fix = SuggestedFix.builder(); + String crvName = qualifyType(state, fix, CRV); + fix.prefixWith(tree, "@" + crvName + "\n"); + return describeMatch(tree, fix.build()); + } + + private static boolean instanceReturnsBuilder(MethodSymbol sym, VisitorState state) { + // instance methods + if (sym.isStatic()) { + return false; + } + // declared in a class with the simple name that contains Builder + ClassSymbol enclosingClass = sym.owner.enclClass(); + if (!enclosingClass.getSimpleName().toString().endsWith("Builder")) { + return false; + } + // whose return type is the exact type of this + // or perhaps "a non-Object supertype of the this-type", for interfaces + Type returnType = sym.getReturnType(); + if (!isSubtype(enclosingClass.asType(), returnType, state) + || isSameType(returnType, state.getSymtab().objectType, state)) { + return false; + } + return true; + } + + // TODO(b/236055787): consolidate heuristics for 'return this;' + boolean nonThisReturns(MethodTree tree, VisitorState state) { + + boolean[] result = {false}; + new TreeScanner() { + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void unused) { + return null; + } + + @Override + public Void visitMethod(MethodTree tree, Void unused) { + return null; + } + + @Override + public Void visitReturn(ReturnTree tree, Void unused) { + if (!returnsThis(tree.getExpression())) { + result[0] = true; + } + return super.visitReturn(tree, null); + } + + private boolean returnsThis(ExpressionTree tree) { + return firstNonNull( + new TreeScanner() { + @Override + public Boolean visitIdentifier(IdentifierTree tree, Void unused) { + return tree.getName().contentEquals("this"); + } + + @Override + public Boolean visitMethodInvocation(MethodInvocationTree tree, Void unused) { + return instanceReturnsBuilder(getSymbol(tree), state); + } + + @Override + public Boolean visitConditionalExpression( + ConditionalExpressionTree tree, Void unused) { + return TRUE.equals(tree.getFalseExpression().accept(this, null)) + && TRUE.equals(tree.getTrueExpression().accept(this, null)); + } + + @Override + public Boolean visitParenthesized(ParenthesizedTree tree, Void unused) { + return tree.getExpression().accept(this, null); + } + + @Override + public Boolean visitTypeCast(TypeCastTree tree, Void unused) { + return tree.getExpression().accept(this, null); + } + }.scan(tree, null), + false); + } + }.scan(tree.getBody(), null); + return result[0]; + } +} 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 b123371ba9c..b79c7168e7d 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -416,6 +416,7 @@ import com.google.errorprone.bugpatterns.argumentselectiondefects.ArgumentSelectionDefectChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AssertEqualsArgumentOrderChecker; import com.google.errorprone.bugpatterns.argumentselectiondefects.AutoValueConstructorOrderChecker; +import com.google.errorprone.bugpatterns.checkreturnvalue.BuilderReturnThis; import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester; import com.google.errorprone.bugpatterns.checkreturnvalue.NoCanIgnoreReturnValueOnClasses; import com.google.errorprone.bugpatterns.checkreturnvalue.UsingJsr305CheckReturnValue; @@ -1021,6 +1022,7 @@ public static ScannerSupplier errorChecks() { BinderIdentityRestoredDangerously.class, // TODO: enable this by default. BindingToUnqualifiedCommonType.class, BooleanParameter.class, + BuilderReturnThis.class, CanIgnoreReturnValueSuggester.class, CatchingUnchecked.class, CheckedExceptionNotThrown.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.java new file mode 100644 index 00000000000..f22784e678b --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/BuilderReturnThisTest.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.checkreturnvalue; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BuilderReturnThisTest { + + private final BugCheckerRefactoringTestHelper testHelper = + BugCheckerRefactoringTestHelper.newInstance(BuilderReturnThis.class, getClass()); + + @Test + public void negative() { + testHelper + .addInputLines( + "Test.java", + "class Test {", + " static class TestBuilder {", + " static TestBuilder builder() {", + " return new TestBuilder();", + " }", + " Test build() {", + " return new Test();", + " }", + " TestBuilder setFoo(String foo) {", + " return this;", + " }", + " TestBuilder setBar(String bar) {", + " return this;", + " }", + " TestBuilder setBaz(String baz) {", + " return setFoo(baz).setBar(baz);", + " }", + " TestBuilder setTernary(String baz) {", + " return true ? setFoo(baz) : this;", + " }", + " TestBuilder setCast(String baz) {", + " return (TestBuilder) this;", + " }", + " TestBuilder setParens(String bar) {", + " return (this);", + " }", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void positive() { + testHelper + .addInputLines( + "Test.java", + "class Test {", + " static class TestBuilder {", + " TestBuilder setBar(String bar) {", + " return new TestBuilder();", + " }", + " TestBuilder setTernary(String baz) {", + " return true ? new TestBuilder() : this;", + " }", + " }", + "}") + .addOutputLines( + "Test.java", + "import com.google.errorprone.annotations.CheckReturnValue;", + "class Test {", + " static class TestBuilder {", + " @CheckReturnValue", + " TestBuilder setBar(String bar) {", + " return new TestBuilder();", + " }", + " @CheckReturnValue", + " TestBuilder setTernary(String baz) {", + " return true ? new TestBuilder() : this;", + " }", + " }", + "}") + .doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH); + } +} diff --git a/docs/bugpattern/BuilderReturnThis.md b/docs/bugpattern/BuilderReturnThis.md new file mode 100644 index 00000000000..491e289f498 --- /dev/null +++ b/docs/bugpattern/BuilderReturnThis.md @@ -0,0 +1,34 @@ +This check identifies instance methods in builder classes, and requires that +they either `return this;`, or are explicitly annotated with +`@CheckReturnValue`. + +Instance methods in builders typically return `this`, to allow chaining. +Ignoring this result does not indicate a https://errorprone.info/bugpattern/CheckReturnValue bug. For +example, both of the following are fine: + +```java +Foo.Builder builder = Foo.builder(); +builder.setBar("bar"); // return value is deliberately unused +return builder.build(); +``` + +```java +Foo.Builder builder = + Foo.builder().setBar("bar").build(); +``` + +Rarely, a builder method may return a new instance, which should not be ignored. +This check requires these methods to be annotated with `@CheckReturnValue`: + +```java +class Builder { + @CheckReturnValue + Builder setFoo(String foo) { + return new Builder(foo); // returns a new builder instead of this! + } +} +``` + +This check allows the https://errorprone.info/bugpattern/CheckReturnValue enforcement to assume the +return value of instance methods in builders can safely be ignored, unless the +method is explicitly annotated with `@CheckReturnValue`. From 48594a63ce36eccada5567f32b9c8acaf10fcc8e Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 17 Jun 2022 05:06:47 -0700 Subject: [PATCH 032/102] Remove flag from ImmutableChecker. PiperOrigin-RevId: 455593315 --- .../bugpatterns/threadsafety/ImmutableChecker.java | 9 ++------- 1 file changed, 2 insertions(+), 7 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 f33fcf1cef4..cbdc95fa2a2 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 @@ -101,7 +101,6 @@ public class ImmutableChecker extends BugChecker private final WellKnownMutability wellKnownMutability; private final ImmutableSet immutableAnnotations; - private final boolean handleAnonymousClasses; ImmutableChecker(ImmutableSet immutableAnnotations) { this(ErrorProneFlags.empty(), immutableAnnotations); @@ -114,8 +113,6 @@ 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 @@ -322,7 +319,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { describeClass(matched, sym, annotation, violation)); Type superType = immutableSupertype(sym, state); - if (handleAnonymousClasses && superType != null && isLocal(sym)) { + if (superType != null && isLocal(sym)) { checkClosedTypes(tree, state, superType.tsym, analysis); } @@ -380,9 +377,7 @@ private Description handleAnonymousClass( return NO_MATCH; } - if (handleAnonymousClasses) { - checkClosedTypes(tree, state, superType.tsym, analysis); - } + 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 From 4dab7ea42c67320d2e70b8f7131925c0caaa736f Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Fri, 17 Jun 2022 16:40:11 -0700 Subject: [PATCH 033/102] Relax the restriction that subtypes of @Immutable types are explicitly annotated PiperOrigin-RevId: 455720017 --- .../threadsafety/ImmutableChecker.java | 47 +++++++++++-------- .../threadsafety/ImmutableCheckerTest.java | 6 +-- 2 files changed, 29 insertions(+), 24 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 cbdc95fa2a2..203563e1bb6 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 @@ -50,8 +50,6 @@ import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; import com.google.errorprone.bugpatterns.threadsafety.ThreadSafety.Violation; -import com.google.errorprone.fixes.Fix; -import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; @@ -101,6 +99,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); @@ -113,6 +112,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 @@ -259,11 +260,13 @@ public Description matchClass(ClassTree tree, VisitorState state) { return handleAnonymousClass(tree, state, analysis); } - AnnotationInfo annotation = analysis.getImmutableAnnotation(tree, state); + AnnotationInfo annotation = getImmutableAnnotation(analysis, tree, state); if (annotation == null) { - // If the type isn't annotated we don't check for immutability, but we do - // report an error if it extends/implements any @Immutable-annotated types. - return checkSubtype(tree, state); + // If the type isn't annotated, and doesn't extend anything annotated, there's nothing to do + // An earlier version of the check required an explicit annotation on classes that extended + // @Immutable classes, but didn't enforce the subtyping requirement for interfaces. We now + // don't require the explicit annotations on any subtypes. + return NO_MATCH; } // Special-case visiting declarations of known-immutable types; these uses @@ -319,7 +322,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { describeClass(matched, sym, annotation, violation)); Type superType = immutableSupertype(sym, state); - if (superType != null && isLocal(sym)) { + if (handleAnonymousClasses && superType != null && isLocal(sym)) { checkClosedTypes(tree, state, superType.tsym, analysis); } @@ -377,7 +380,9 @@ private Description handleAnonymousClass( return NO_MATCH; } - checkClosedTypes(tree, state, superType.tsym, analysis); + 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 @@ -540,21 +545,23 @@ private Description.Builder describeAnonymous(Tree tree, Type superType, Violati // Strong behavioural subtyping - /** Check for classes without {@code @Immutable} that have immutable supertypes. */ - private Description checkSubtype(ClassTree tree, VisitorState state) { + /** + * Check for classes with {@code @Immutable}, or that inherited it from a super class or + * interface. + */ + private AnnotationInfo getImmutableAnnotation( + ImmutableAnalysis analysis, ClassTree tree, VisitorState state) { + AnnotationInfo annotation = analysis.getImmutableAnnotation(tree, state); + if (annotation != null) { + return annotation; + } + // getImmutableAnnotation inherits annotations from classes, but not interfaces. ClassSymbol sym = getSymbol(tree); Type superType = immutableSupertype(sym, state); - if (superType == null) { - return NO_MATCH; + if (superType != null) { + return analysis.getImmutableAnnotation(superType.tsym, state); } - String message = - format("Class extends @Immutable type %s, but is not annotated as immutable", superType); - Fix fix = - SuggestedFix.builder() - .prefixWith(tree, "@Immutable ") - .addImport(Immutable.class.getName()) - .build(); - return buildDescription(tree).setMessage(message).addFix(fix).build(); + return 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 864524e5e8e..82dd6e1b109 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 @@ -133,9 +133,8 @@ public void customAnnotationsSubtype() { .addSourceLines( "MyTest.java", "import java.lang.annotation.Annotation;", - "// BUG: Diagnostic contains:", - "// extends @Immutable type Test, but is not annotated as immutable", "final class MyTest implements Test {", + " // BUG: Diagnostic contains: non-final field 'xs'", " public Object[] xs = {};", " public Class annotationType() {", " return null;", @@ -1062,14 +1061,13 @@ public void transitive() { .addSourceLines( "threadsafety/Test.java", "package threadsafety;", - "// BUG: Diagnostic contains: extends @Immutable", "class Test implements J {", + " // BUG: Diagnostic contains: non-final field 'x'", " public int x = 0;", "}") .addSourceLines( "threadsafety/J.java", // "package threadsafety;", - "// BUG: Diagnostic contains: extends @Immutable", "interface J extends I {", "}") .doTest(); From 2d2b6a1b62b6065f4a0ee6780343f997eaca4692 Mon Sep 17 00:00:00 2001 From: ghm Date: Sat, 18 Jun 2022 07:38:04 -0700 Subject: [PATCH 034/102] Resolve merge conflicts from https://github.com/google/error-prone/commit/4dab7ea42c67320d2e70b8f7131925c0caaa736f PiperOrigin-RevId: 455807847 --- .../bugpatterns/threadsafety/ImmutableChecker.java | 11 +++-------- 1 file changed, 3 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 203563e1bb6..6fbb5d0ebfb 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 @@ -99,7 +99,6 @@ 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,8 +111,6 @@ 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 @@ -262,7 +259,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { AnnotationInfo annotation = getImmutableAnnotation(analysis, tree, state); if (annotation == null) { - // If the type isn't annotated, and doesn't extend anything annotated, there's nothing to do + // If the type isn't annotated, and doesn't extend anything annotated, there's nothing to do. // An earlier version of the check required an explicit annotation on classes that extended // @Immutable classes, but didn't enforce the subtyping requirement for interfaces. We now // don't require the explicit annotations on any subtypes. @@ -322,7 +319,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { describeClass(matched, sym, annotation, violation)); Type superType = immutableSupertype(sym, state); - if (handleAnonymousClasses && superType != null && isLocal(sym)) { + if (superType != null && isLocal(sym)) { checkClosedTypes(tree, state, superType.tsym, analysis); } @@ -380,9 +377,7 @@ private Description handleAnonymousClass( return NO_MATCH; } - if (handleAnonymousClasses) { - checkClosedTypes(tree, state, superType.tsym, analysis); - } + 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 From 6f631c1824423e9b8dceb3277e18e42fd08dcacb Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 21 Jun 2022 19:53:44 -0700 Subject: [PATCH 035/102] Don't suggest `@CanIgnoreReturnValue` if the method body does nothing other than `return this`. Via b/236423646#comment4: "[CIRVSuggester] should not have included methods that only do `return this`. `@CIRV` makes sense when the method has some side-effects and then `returns this` just for convenience, like your typical builder." PiperOrigin-RevId: 456411570 --- .../CanIgnoreReturnValueSuggester.java | 47 ++++---- .../CanIgnoreReturnValueSuggesterTest.java | 102 ++++++++++++++---- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java index f89a6169a0b..5379dfb2685 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -38,6 +38,7 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePathScanner; import com.sun.tools.javac.code.Symbol.MethodSymbol; @@ -97,6 +98,16 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { return Description.NO_MATCH; } + // if the method doesn't do anything but "return this", bail out; see b/236423646 + if (methodTree.getBody().getStatements().size() == 1) { + StatementTree onlyStatement = methodTree.getBody().getStatements().get(0); + if (onlyStatement instanceof ReturnTree) { + if (returnsThis((ReturnTree) onlyStatement, state)) { + return Description.NO_MATCH; + } + } + } + // if the method is already directly annotated w/ @CIRV, bail out if (hasAnnotation(methodTree, CIRV, state)) { return Description.NO_MATCH; @@ -126,7 +137,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { @Override public Void visitReturn(ReturnTree returnTree, Void unused) { atLeastOneReturn.set(true); - if (!returnsThis(returnTree)) { + if (!returnsThis(returnTree, state)) { allReturnThis.set(false); // once we've set allReturnThis to false, no need to descend further return null; @@ -134,23 +145,6 @@ public Void visitReturn(ReturnTree returnTree, Void unused) { return super.visitReturn(returnTree, null); } - /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */ - private boolean returnsThis(ReturnTree returnTree) { - ExpressionTree returnExpression = returnTree.getExpression(); - if (returnExpression instanceof IdentifierTree) { - if (((IdentifierTree) returnExpression).getName().contentEquals("this")) { - return true; - } - } - if (returnExpression instanceof MethodInvocationTree) { - MethodInvocationTree mit = (MethodInvocationTree) returnExpression; - if (state.getSourceForNode(mit.getMethodSelect()).contentEquals("self")) { - return true; - } - } - return false; - } - @Override public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { // don't descend into lambdas @@ -177,6 +171,23 @@ public Void visitNewClass(NewClassTree node, Void unused) { return Description.NO_MATCH; } + /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */ + private static boolean returnsThis(ReturnTree returnTree, VisitorState state) { + ExpressionTree returnExpression = returnTree.getExpression(); + if (returnExpression instanceof IdentifierTree) { + if (((IdentifierTree) returnExpression).getName().contentEquals("this")) { + return true; + } + } + if (returnExpression instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) returnExpression; + if (state.getSourceForNode(mit.getMethodSelect()).contentEquals("self")) { + return true; + } + } + return false; + } + private static boolean isProtoBuilderSubtype(Type ownerType, VisitorState state) { return isSubtype(ownerType, PROTO_BUILDER.get(state), state); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java index f99cae79e89..e01941c12f5 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -35,7 +35,9 @@ public void testSimpleCase() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") @@ -44,8 +46,10 @@ public void testSimpleCase() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") @@ -59,7 +63,9 @@ public void testReturnSelf_b234875737() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " return self();", " }", " private Client self() {", @@ -71,8 +77,10 @@ public void testReturnSelf_b234875737() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return self();", " }", " private Client self() {", @@ -90,8 +98,10 @@ public void testSimpleCaseAlreadyAnnotatedWithCirv() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") @@ -107,8 +117,10 @@ public void testSimpleCaseAlreadyAnnotatedWithCrv() { "package com.google.frobber;", "import com.google.errorprone.annotations.CheckReturnValue;", "public final class Client {", + " private String name;", " @CheckReturnValue", // this is "wrong" -- the checker could fix it though! - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") @@ -124,7 +136,8 @@ public void testSimpleCaseWithNestedLambda() { "package com.google.frobber;", "import java.util.function.Function;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", " new Function() {", " @Override", " public String apply(String in) {", @@ -140,8 +153,9 @@ public void testSimpleCaseWithNestedLambda() { "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "import java.util.function.Function;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", " new Function() {", " @Override", " public String apply(String in) {", @@ -161,7 +175,9 @@ public void testAnotherMethodDoesntReturnThis() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue1() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", " public Client getValue2() {", @@ -173,8 +189,10 @@ public void testAnotherMethodDoesntReturnThis() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue1() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", " public Client getValue2() {", @@ -191,7 +209,9 @@ public void testNestedCase() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " if (true) {", " return new Client();", " }", @@ -209,7 +229,9 @@ public void testNestedCaseBothReturningThis() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " if (true) {", " return this;", " }", @@ -221,8 +243,10 @@ public void testNestedCaseBothReturningThis() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " if (true) {", " return this;", " }", @@ -283,7 +307,9 @@ public void testSometimesThrows() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", + " this.name = name;", " if (true) throw new UnsupportedOperationException();", " return this;", " }", @@ -293,8 +319,10 @@ public void testSometimesThrows() { "package com.google.frobber;", "import com.google.errorprone.annotations.CanIgnoreReturnValue;", "public final class Client {", + " private String name;", " @CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " if (true) throw new UnsupportedOperationException();", " return this;", " }", @@ -309,7 +337,8 @@ public void testAlwaysThrows() { "Client.java", "package com.google.frobber;", "public final class Client {", - " public Client getValue() {", + " private String name;", + " public Client setName(String name) {", " throw new UnsupportedOperationException();", " }", "}") @@ -324,8 +353,10 @@ public void testSimpleCaseWithSimpleNameConflict() { "Client.java", "package com.google.frobber;", "public final class Client {", + " private String name;", " public @interface CanIgnoreReturnValue {}", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") @@ -333,12 +364,47 @@ public void testSimpleCaseWithSimpleNameConflict() { "Client.java", "package com.google.frobber;", "public final class Client {", + " private String name;", " public @interface CanIgnoreReturnValue {}", " @com.google.errorprone.annotations.CanIgnoreReturnValue", - " public Client getValue() {", + " public Client setName(String name) {", + " this.name = name;", " return this;", " }", "}") .doTest(); } + + @Test + public void testOnlyReturnsThis_b236423646() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getFoo() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } + + @Test + public void testOnlyReturnsSelf_b236423646() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "public final class Client {", + " public Client getFoo() {", + " return self();", + " }", + " public Client self() {", + " return this;", + " }", + "}") + .expectUnchanged() + .doTest(); + } } From 0ad047b60e0b4f90ef78d2963a7738e0afbcce55 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 22 Jun 2022 02:35:24 -0700 Subject: [PATCH 036/102] CannotMockFinalMethod: flag where when(...) is called on a final method. Flume: unknown commit PiperOrigin-RevId: 456465825 --- .../bugpatterns/CannotMockFinalMethod.java | 52 ++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../CannotMockFinalMethodTest.java | 81 +++++++++++++++++++ docs/bugpattern/CannotMockFinalMethod.md | 12 +++ 4 files changed, 147 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java create mode 100644 docs/bugpattern/CannotMockFinalMethod.md diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java new file mode 100644 index 00000000000..80d935ae4eb --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java @@ -0,0 +1,52 @@ +/* + * 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.matchers.Matchers.staticMethod; +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.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.tools.javac.code.Flags; + +/** A BugPattern; see the summary */ +@BugPattern( + summary = "Mockito cannot mock final methods, and can't detect this at runtime", + severity = WARNING) +public final class CannotMockFinalMethod extends BugChecker implements MethodInvocationTreeMatcher { + + private static final Matcher VERIFY_WHEN = + staticMethod().onClass("org.mockito.Mockito").namedAnyOf("verify", "when"); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (!VERIFY_WHEN.matches(tree, state)) { + return NO_MATCH; + } + ExpressionTree firstArgument = tree.getArguments().get(0); + if (!(firstArgument instanceof MethodInvocationTree)) { + return NO_MATCH; + } + var methodSymbol = getSymbol((MethodInvocationTree) firstArgument); + return (methodSymbol.flags() & Flags.FINAL) == 0 ? NO_MATCH : 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 b79c7168e7d..7d9f8ee4198 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -61,6 +61,7 @@ import com.google.errorprone.bugpatterns.ByteBufferBackingArray; import com.google.errorprone.bugpatterns.CacheLoaderNull; import com.google.errorprone.bugpatterns.CannotMockFinalClass; +import com.google.errorprone.bugpatterns.CannotMockFinalMethod; import com.google.errorprone.bugpatterns.CanonicalDuration; import com.google.errorprone.bugpatterns.CatchAndPrintStackTrace; import com.google.errorprone.bugpatterns.CatchFail; @@ -800,6 +801,7 @@ public static ScannerSupplier errorChecks() { ByteBufferBackingArray.class, CacheLoaderNull.class, CannotMockFinalClass.class, + CannotMockFinalMethod.class, CanonicalDuration.class, CatchAndPrintStackTrace.class, CatchFail.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java new file mode 100644 index 00000000000..aba94d7c324 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java @@ -0,0 +1,81 @@ +/* + * 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; + +@RunWith(JUnit4.class) +public final class CannotMockFinalMethodTest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(CannotMockFinalMethod.class, getClass()); + + @Test + public void whenCall_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.when;", + "class Test {", + " final Integer foo() {", + " return 1;", + " }", + " void test() {", + " // BUG: Diagnostic contains:", + " when(this.foo());", + " }", + "}") + .doTest(); + } + + @Test + public void verifyCall_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.verify;", + "class Test {", + " final Integer foo() {", + " return 1;", + " }", + " void test() {", + " // BUG: Diagnostic contains:", + " verify(this.foo());", + " }", + "}") + .doTest(); + } + + @Test + public void negative() { + compilationHelper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.when;", + "class Test {", + " Integer foo() {", + " return 1;", + " }", + " void test() {", + " when(this.foo());", + " }", + "}") + .doTest(); + } +} diff --git a/docs/bugpattern/CannotMockFinalMethod.md b/docs/bugpattern/CannotMockFinalMethod.md new file mode 100644 index 00000000000..d47bc98d725 --- /dev/null +++ b/docs/bugpattern/CannotMockFinalMethod.md @@ -0,0 +1,12 @@ +Mockito cannot mock `final` methods, and cannot tell at runtime that this is +attempted and fail with an error (as mocking `final` classes does). + +`when(mock.finalMethod())` will invoke the real implementation of `finalMethod`. +In some cases, this may wind up accidentally doing what's intended: + +```java +when(converter.convert(a)).thenReturn(b); +``` + +`convert` is final, but under the hood, calls `doForward`, so we wind up mocking +that method instead. From 426c136b1ed1daee8c25aa064e910634eaf18600 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 22 Jun 2022 08:22:39 -0700 Subject: [PATCH 037/102] DirectInvocationOnMock: ignore invocations of final methods, given those can't be mocked. I don't love this as a heuristic (I don't think even final methods really should be invoked on a mock), but I like it a bit more than extra-special casing getClass. PiperOrigin-RevId: 456517627 --- .../bugpatterns/DirectInvocationOnMock.java | 4 ++++ .../bugpatterns/DirectInvocationOnMockTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java index 73e578ee5ed..ca525f7fc9e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -43,6 +43,7 @@ import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol.VarSymbol; /** A bugpattern; see the description. */ @@ -59,6 +60,9 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s new SuppressibleTreePathScanner(state) { @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { + if ((getSymbol(tree).flags() & Flags.FINAL) != 0) { + return null; + } Tree parent = stream(getCurrentPath()) .skip(1) diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java index 2f3403372d1..fc1772d616b 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -191,4 +191,19 @@ public void directInvocationOnMock_withinWhenWithCast_noFinding() { "}") .doTest(); } + + @Test + public void finalMethodInvoked_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " return test.getClass();", + " }", + "}") + .doTest(); + } } From 8dae573ccf9fe3b691de83828c1c86db7cbd8fe9 Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Wed, 22 Jun 2022 12:13:23 -0700 Subject: [PATCH 038/102] Add a rule for matching packages. RELNOTES=n/a PiperOrigin-RevId: 456576706 --- .../checkreturnvalue/PackagesRule.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java new file mode 100644 index 00000000000..c23ffca5e9c --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java @@ -0,0 +1,66 @@ +/* + * 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.common.collect.ImmutableMap; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.SymbolRule; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.PackageSymbol; +import com.sun.tools.javac.util.Name; +import java.util.Optional; + +/** + * A rule that enables checking for methods belonging to a set of packages or any of their + * subpackages. + */ +final class PackagesRule extends SymbolRule { + + private final ImmutableMap packages; + + public PackagesRule(VisitorState state, Iterable patterns) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (String pattern : patterns) { + if (pattern.charAt(0) == '-') { + builder.put(state.getName(pattern.substring(1)), false); + } else { + builder.put(state.getName(pattern), true); + } + } + this.packages = builder.buildOrThrow(); + } + + @Override + public final String id() { + return "Packages"; + } + + @Override + public Optional evaluate(Symbol symbol, VisitorState state) { + while (symbol instanceof PackageSymbol) { + Boolean value = packages.get(((PackageSymbol) symbol).fullname); + if (value != null) { + return value + ? Optional.of(ResultUsePolicy.EXPECTED) + // stop evaluating if the package matched a negative pattern + : Optional.empty(); + } + symbol = symbol.owner; + } + return Optional.empty(); + } +} From 7c9cb10d8210300c856b20360933554eea44e720 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 23 Jun 2022 06:36:01 -0700 Subject: [PATCH 039/102] Disable CannotMockFinalClass/Method in open-source. We want this enabled internally, but externally, Mockito can be configured to allow mocking `final` classes (and methods). It's probably a bad idea to default it on. Fixes https://github.com/google/error-prone/issues/3281 PiperOrigin-RevId: 456754155 --- .../google/errorprone/scanner/BuiltInCheckerSuppliers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7d9f8ee4198..9bdb46394f8 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -800,8 +800,6 @@ public static ScannerSupplier errorChecks() { BugPatternNaming.class, ByteBufferBackingArray.class, CacheLoaderNull.class, - CannotMockFinalClass.class, - CannotMockFinalMethod.class, CanonicalDuration.class, CatchAndPrintStackTrace.class, CatchFail.class, @@ -1026,6 +1024,8 @@ public static ScannerSupplier errorChecks() { BooleanParameter.class, BuilderReturnThis.class, CanIgnoreReturnValueSuggester.class, + CannotMockFinalClass.class, + CannotMockFinalMethod.class, CatchingUnchecked.class, CheckedExceptionNotThrown.class, ClassName.class, From 9e597c361503d6c670cc84856bff8981c420e27d Mon Sep 17 00:00:00 2001 From: Colin Decker Date: Thu, 23 Jun 2022 08:52:28 -0700 Subject: [PATCH 040/102] Add package-prefix based checking to `CheckReturnValue`. RELNOTES=n/a PiperOrigin-RevId: 456781343 --- .../bugpatterns/CheckReturnValue.java | 37 +++++++---- .../checkreturnvalue/PackagesRule.java | 42 ++++++++---- .../bugpatterns/CheckReturnValueTest.java | 64 +++++++++++++++++++ 3 files changed, 119 insertions(+), 24 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 1d12464638f..e96de62dbf5 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -37,6 +37,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.PackagesRule; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator; import com.google.errorprone.matchers.Description; @@ -70,6 +71,8 @@ public class CheckReturnValue extends AbstractReturnValueIgnored static final String CHECK_ALL_CONSTRUCTORS = "CheckReturnValue:CheckAllConstructors"; static final String CHECK_ALL_METHODS = "CheckReturnValue:CheckAllMethods"; + static final String CRV_PACKAGES = "CheckReturnValue:Packages"; + private final Optional constructorPolicy; private final Optional methodPolicy; private final ResultUsePolicyEvaluator evaluator; @@ -79,17 +82,29 @@ public CheckReturnValue(ErrorProneFlags flags) { this.constructorPolicy = defaultPolicy(flags, CHECK_ALL_CONSTRUCTORS); this.methodPolicy = defaultPolicy(flags, CHECK_ALL_METHODS); - this.evaluator = - ResultUsePolicyEvaluator.create( - mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), - mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), - protoBuilders(), - mutableProtos(), - autoValues(), - autoValueBuilders(), - autoBuilders(), - externalIgnoreList(), - globalDefault(methodPolicy, constructorPolicy)); + ResultUsePolicyEvaluator.Builder builder = + ResultUsePolicyEvaluator.builder() + .addRules( + // The order of these rules matters somewhat because when checking a method, we'll + // evaluate them in the order they're listed here and stop as soon as one of them + // returns a result. The order shouldn't matter because most of these, with the + // exception of perhaps the external ignore list, are equivalent in importance and + // we should be checking declarations to ensure they aren't producing differing + // results (i.e. ensuring an @AutoValue.Builder setter method isn't annotated @CRV). + mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), + mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), + protoBuilders(), + mutableProtos(), + autoValues(), + autoValueBuilders(), + autoBuilders(), + + // This is conceptually lower precedence than the above rules. + externalIgnoreList()); + flags + .getList(CRV_PACKAGES) + .ifPresent(packagePatterns -> builder.addRule(PackagesRule.fromPatterns(packagePatterns))); + this.evaluator = builder.addRule(globalDefault(methodPolicy, constructorPolicy)).build(); } private static Optional defaultPolicy(ErrorProneFlags flags, String flag) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java index c23ffca5e9c..4402771e897 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/PackagesRule.java @@ -16,9 +16,11 @@ package com.google.errorprone.bugpatterns.checkreturnvalue; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.SymbolRule; +import com.google.errorprone.suppliers.Supplier; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.PackageSymbol; import com.sun.tools.javac.util.Name; @@ -28,20 +30,34 @@ * A rule that enables checking for methods belonging to a set of packages or any of their * subpackages. */ -final class PackagesRule extends SymbolRule { +public final class PackagesRule extends SymbolRule { - private final ImmutableMap packages; + /** + * Returns a new rule using the given package {@code patterns}. Each pattern string must either be + * the fully qualified name of a package (to enable checking for methods in that package and its + * subpackages) or a {@code -} character followed by the fully qualified name of a package (to + * disable checking for methods in that package and its subpackages). + */ + public static PackagesRule fromPatterns(Iterable patterns) { + return new PackagesRule(ImmutableList.copyOf(patterns)); + } - public PackagesRule(VisitorState state, Iterable patterns) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (String pattern : patterns) { - if (pattern.charAt(0) == '-') { - builder.put(state.getName(pattern.substring(1)), false); - } else { - builder.put(state.getName(pattern), true); - } - } - this.packages = builder.buildOrThrow(); + private final Supplier> packagesSupplier; + + private PackagesRule(ImmutableList patterns) { + this.packagesSupplier = + VisitorState.memoize( + state -> { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (String pattern : patterns) { + if (pattern.charAt(0) == '-') { + builder.put(state.getName(pattern.substring(1)), false); + } else { + builder.put(state.getName(pattern), true); + } + } + return builder.buildOrThrow(); + }); } @Override @@ -52,7 +68,7 @@ public final String id() { @Override public Optional evaluate(Symbol symbol, VisitorState state) { while (symbol instanceof PackageSymbol) { - Boolean value = packages.get(((PackageSymbol) symbol).fullname); + Boolean value = packagesSupplier.get(state).get(((PackageSymbol) symbol).fullname); if (value != null) { return value ? Optional.of(ResultUsePolicy.EXPECTED) 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 298fa258553..388f79fc8e1 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -1042,6 +1042,64 @@ public void testAutoBuilderSetterMethods_withInterface() { .doTest(); } + @Test + public void testPackagesRule() { + compilationHelperWithPackagePatterns("java.util") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: Ignored return value of 'get'", + " list.get(0);", + " // BUG: Diagnostic contains: Ignored return value of 'matcher'", + " pattern.matcher(\"blah\");", + " }", + "}") + .doTest(); + } + + @Test + public void testPackagesRule_negativePattern() { + compilationHelperWithPackagePatterns("java.util", "-java.util.regex") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: Ignored return value of 'get'", + " list.get(0);", + " pattern.matcher(\"blah\");", + " }", + "}") + .doTest(); + } + + @Test + public void testPackagesRule_negativePattern_doesNotMakeOptional() { + // A negative pattern just makes the packages rule itself not apply to that package and its + // subpackages if it otherwise would because of a positive pattern on a superpackage. It doesn't + // make APIs in that package CIRV. + compilationHelperWithPackagePatterns("java.util", "-java.util.regex") + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.regex.Pattern;", + "import java.util.regex.PatternSyntaxException;", + "class Test {", + " public static void foo(List list, Pattern pattern) {", + " // BUG: Diagnostic contains: Ignored return value of 'get'", + " list.get(0);", + " pattern.matcher(\"blah\");", + " // BUG: Diagnostic contains: Ignored return value", + " new PatternSyntaxException(\"\", \"\", 0);", + " }", + "}") + .doTest(); + } + private CompilationTestHelper compilationHelperLookingAtAllConstructors() { return compilationHelper.setArgs( "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); @@ -1063,4 +1121,10 @@ private CompilationTestHelper compileWithExternalApis(String... apis) { throw new UncheckedIOException(e); } } + + private CompilationTestHelper compilationHelperWithPackagePatterns(String... patterns) { + return compilationHelper.setArgs( + "-XepOpt:" + CheckReturnValue.CRV_PACKAGES + "=" + Joiner.on(',').join(patterns), + "-XepOpt:" + CheckReturnValue.CHECK_ALL_CONSTRUCTORS + "=true"); + } } From 4c7dff57339bba6b4cf41fc776a51de6c6bd5e00 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Thu, 23 Jun 2022 18:08:01 -0700 Subject: [PATCH 041/102] Improve performance of Unicode-detecting checkers - UnicodeInCode now only computes comment ranges if it finds a funky character. Most of the time this is a big win, because that computation is expensive and most files don't have weird characters. - Inlined a method inUnicodeDirectionalityCharacters. Apparently the loop is hot enough that the overhead of a single method call was doubling(!?) the total execution time PiperOrigin-RevId: 456903383 --- .../UnicodeDirectionalityCharacters.java | 42 +++++++++---------- .../errorprone/bugpatterns/UnicodeInCode.java | 17 ++++---- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java index 8d1b682e8a4..f0cdf0818fd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeDirectionalityCharacters.java @@ -40,30 +40,28 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s for (int i = 0; i < source.length(); ++i) { char c = source.charAt(i); - if (isDangerous(c)) { - state.reportMatch( - describeMatch( - new FixedPosition(tree, i), - SuggestedFix.replace(i, i + 1, String.format("\\u%04x", (int) c)))); + // Do not extract this switch to a method. It's ugly as-is, but profiling suggests this + // checker is expensive for large files, and also that the method-call overhead would + // double the time spent in this loop. + switch (c) { + case 0x202A: // Left-to-Right Embedding + case 0x202B: // Right-to-Left Embedding + case 0x202C: // Pop Directional Formatting + case 0x202D: // Left-to-Right Override + case 0x202E: // Right-to-Left Override + case 0x2066: // Left-to-Right Isolate + case 0x2067: // Right-to-Left Isolate + case 0x2068: // First Strong Isolate + case 0x2069: // Pop Directional Isolate + state.reportMatch( + describeMatch( + new FixedPosition(tree, i), + SuggestedFix.replace(i, i + 1, String.format("\\u%04x", (int) c)))); + break; + default: + break; } } return NO_MATCH; } - - private static boolean isDangerous(char c) { - switch (c) { - case 0x202A: // Left-to-Right Embedding - case 0x202B: // Right-to-Left Embedding - case 0x202C: // Pop Directional Formatting - case 0x202D: // Left-to-Right Override - case 0x202E: // Right-to-Left Override - case 0x2066: // Left-to-Right Isolate - case 0x2067: // Right-to-Left Isolate - case 0x2068: // First Strong Isolate - case 0x2069: // Pop Directional Isolate - return true; - default: - return false; - } - } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java index 2a2c0fa77df..98ee491645d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java @@ -47,16 +47,13 @@ public final class UnicodeInCode extends BugChecker implements CompilationUnitTreeMatcher { @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { - ImmutableRangeSet commentsAndLiterals = commentsAndLiterals(state); - Map violations = new LinkedHashMap<>(); - - CharSequence sourceCode = state.getSourceCode(); + String sourceCode = state.getSourceCode().toString(); for (int i = 0; i < sourceCode.length(); ++i) { char c = sourceCode.charAt(i); - if (!isAcceptableAscii(c) && !commentsAndLiterals.contains(i)) { + if (!isAcceptableAscii(c)) { violations.put(i, c); } } @@ -65,12 +62,13 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s return NO_MATCH; } - ImmutableRangeSet suppressedRegions = suppressedRegions(state); + ImmutableRangeSet permissibleUnicodeRegions = + suppressedRegions(state).union(commentsAndLiterals(state, sourceCode)); for (var e : violations.entrySet()) { int violatingLocation = e.getKey(); char c = e.getValue(); - if (!suppressedRegions.contains(violatingLocation)) { + if (!permissibleUnicodeRegions.contains(violatingLocation)) { state.reportMatch( buildDescription(new FixedPosition(tree, violatingLocation)) .setMessage( @@ -88,9 +86,8 @@ private static boolean isAcceptableAscii(char c) { return (c >= 0x20 && c <= 0x7E) || c == '\n' || c == '\r' || c == '\t'; } - private static ImmutableRangeSet commentsAndLiterals(VisitorState state) { - ImmutableList tokens = - getTokens(state.getSourceCode().toString(), state.context); + private static ImmutableRangeSet commentsAndLiterals(VisitorState state, String source) { + ImmutableList tokens = getTokens(source, state.context); return ImmutableRangeSet.unionOf( concat( tokens.stream() From 6f563b250786f42587b111c6004aabefa381ff0c Mon Sep 17 00:00:00 2001 From: Error Prone Team Date: Thu, 23 Jun 2022 20:03:32 -0700 Subject: [PATCH 042/102] small corrections to the sample code was missing String decl PiperOrigin-RevId: 456916692 --- .../com/google/errorprone/annotations/CompileTimeConstant.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java index 8fc04f31b51..aae9ea47837 100644 --- a/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java +++ b/annotations/src/main/java/com/google/errorprone/annotations/CompileTimeConstant.java @@ -47,7 +47,7 @@ * *
{@code
  * public class C {
- *   private static final S = "Hello";
+ *   private static final String S = "Hello";
  *   void m(@CompileTimeConstant final String s) { }
  *   void n(@CompileTimeConstant final String t) {
  *     m(S + " World!");

From 502467f00ef9433533a0820732934342c64b6c99 Mon Sep 17 00:00:00 2001
From: cpovirk 
Date: Fri, 24 Jun 2022 09:27:24 -0700
Subject: [PATCH 043/102] Migrate from legacy com.google.[] to org.[]project.

org.[]project starts being available at the 2.10.0 release, so upgrade to that where necessary.

See https://github.com/google/auto/pull/1342#issuecomment-1165230080

PiperOrigin-RevId: 457027428
---
 core/pom.xml | 2 +-
 pom.xml      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/pom.xml b/core/pom.xml
index a968768423a..e764a81a419 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -128,7 +128,7 @@
     
     
       
-      com.google.gwt
+      org.gwtproject
       gwt-user
       ${gwt.version}
       test
diff --git a/pom.xml b/pom.xml
index f6e3708749b..e8e765ead4f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
   
     UTF-8
     31.0.1-jre
-    2.8.2
+    2.10.0
     1.1.3
     1.0.1
     1.9

From 759862317efa4d6551b55ae961d6a85c1648078b Mon Sep 17 00:00:00 2001
From: Alan Malloy 
Date: Fri, 24 Jun 2022 10:55:35 -0700
Subject: [PATCH 044/102] Fix quadratic-time performance in ParameterName

It used to tokenize the source of the entire MethodInvocationTree, with the result that f(g(h(i(x)))) tokenized g twice, h three times, and i four times (in four different invocations of ParameterName.matchMethodInvocation).

Instead, we now tokenize only parts of the source file immediately preceding each argument of the current invocation.

This helps a lot for degenerate source files with very long fluent chains or deeply nested function arguments.

PiperOrigin-RevId: 457048049
---
 .../errorprone/bugpatterns/ParameterName.java | 98 ++++++++++++-------
 1 file changed, 62 insertions(+), 36 deletions(-)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
index 9cc7890541f..0b18a605a70 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.Iterables.getLast;
-import static com.google.common.collect.Streams.forEachPair;
 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;
@@ -52,7 +50,10 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 
 /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
@@ -80,18 +81,21 @@ public ParameterName(ErrorProneFlags errorProneFlags) {
 
   @Override
   public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
-    checkArguments(tree, tree.getArguments(), state);
+    checkArguments(tree, tree.getArguments(), state.getEndPosition(tree.getMethodSelect()), state);
     return NO_MATCH;
   }
 
   @Override
   public Description matchNewClass(NewClassTree tree, VisitorState state) {
-    checkArguments(tree, tree.getArguments(), state);
+    checkArguments(tree, tree.getArguments(), state.getEndPosition(tree.getIdentifier()), state);
     return NO_MATCH;
   }
 
   private void checkArguments(
-      Tree tree, List arguments, VisitorState state) {
+      Tree tree,
+      List arguments,
+      int argListStartPosition,
+      VisitorState state) {
     if (arguments.isEmpty()) {
       return;
     }
@@ -99,55 +103,77 @@ private void checkArguments(
     if (NamedParameterComment.containsSyntheticParameterName(sym)) {
       return;
     }
-    int start = getStartPosition(tree);
-    int end = state.getEndPosition(getLast(arguments));
-    if (start == Position.NOPOS || end == Position.NOPOS) {
+    int start = argListStartPosition;
+    if (start == Position.NOPOS) {
       // best effort work-around for https://github.com/google/error-prone/issues/780
       return;
     }
-    String source = state.getSourceCode().subSequence(start, end).toString();
-    if (!source.contains("/*")) {
-      // fast path if the arguments don't contain anything that looks like a comment
-      return;
-    }
     String enclosingClass = ASTHelpers.enclosingClass(sym).toString();
     if (exemptPackages.stream().anyMatch(enclosingClass::startsWith)) {
       return;
     }
-    Deque tokens =
-        new ArrayDeque<>(ErrorProneTokens.getTokens(source, start, state.context));
-    forEachPair(
-        sym.getParameters().stream(),
-        arguments.stream(),
-        (p, a) -> {
-          if (advanceTokens(tokens, a, state)) {
-            checkArgument(p, a, tokens.removeFirst(), state);
-          }
-        });
+    Iterator argumentIterator = arguments.iterator();
+    // For each parameter/argument pair, we tokenize the characters between the end of the
+    // previous argument (or the start of the argument list, in the case of the first argument)
+    // and the start of the current argument. The `start` variable is advanced each time, stepping
+    // over each argument when we finish processing it.
+    for (VarSymbol param : sym.getParameters()) {
+      if (!argumentIterator.hasNext()) {
+        return; // A vararg parameter has zero corresponding arguments passed
+      }
+      ExpressionTree argument = argumentIterator.next();
+      Optional> positions = positions(argument, state);
+      if (positions.isEmpty()) {
+        return;
+      }
+      start =
+          processArgument(
+              positions.get(), start, state, tok -> checkArgument(param, argument, tok, state));
+    }
 
     // handle any varargs arguments after the first
-    int numParams = sym.getParameters().size();
-    int numArgs = arguments.size();
-    if (numParams < numArgs) {
-      for (ExpressionTree arg : arguments.subList(numParams, numArgs)) {
-        if (advanceTokens(tokens, arg, state)) {
-          checkComment(arg, tokens.removeFirst(), state);
-        }
+    while (argumentIterator.hasNext()) {
+      ExpressionTree argument = argumentIterator.next();
+      Optional> positions = positions(argument, state);
+      if (positions.isEmpty()) {
+        return;
       }
+      start =
+          processArgument(positions.get(), start, state, tok -> checkComment(argument, tok, state));
+    }
+  }
+
+  /** Returns the source span for a tree, or empty if the position information is not available. */
+  Optional> positions(Tree tree, VisitorState state) {
+    int endPosition = state.getEndPosition(tree);
+    if (endPosition == Position.NOPOS) {
+      return Optional.empty();
+    }
+    return Optional.of(Range.closedOpen(getStartPosition(tree), endPosition));
+  }
+
+  private static int processArgument(
+      Range positions,
+      int offset,
+      VisitorState state,
+      Consumer consumer) {
+    String source = state.getSourceCode().subSequence(offset, positions.upperEndpoint()).toString();
+    Deque tokens =
+        new ArrayDeque<>(ErrorProneTokens.getTokens(source, offset, state.context));
+    if (advanceTokens(tokens, positions)) {
+      consumer.accept(tokens.removeFirst());
     }
+    return positions.upperEndpoint();
   }
 
-  private static boolean advanceTokens(
-      Deque tokens, ExpressionTree actual, VisitorState state) {
-    while (!tokens.isEmpty() && tokens.getFirst().pos() < getStartPosition(actual)) {
+  private static boolean advanceTokens(Deque tokens, Range actual) {
+    while (!tokens.isEmpty() && tokens.getFirst().pos() < actual.lowerEndpoint()) {
       tokens.removeFirst();
     }
     if (tokens.isEmpty()) {
       return false;
     }
-    Range argRange =
-        Range.closedOpen(getStartPosition(actual), state.getEndPosition(actual));
-    if (!argRange.contains(tokens.getFirst().pos())) {
+    if (!actual.contains(tokens.getFirst().pos())) {
       return false;
     }
     return true;

From adbcd3392e2ba9b6aae2b1c2ae9ea10d9d733843 Mon Sep 17 00:00:00 2001
From: ghm 
Date: Fri, 24 Jun 2022 17:37:44 -0700
Subject: [PATCH 045/102] Exclude default methods from MethodCanBeStatic.

This is a bit over-zealous, but I can't imagine it often being a useful finding.

Fixes external https://github.com/google/error-prone/issues/3090

PiperOrigin-RevId: 457126278
---
 .../google/errorprone/bugpatterns/MethodCanBeStatic.java  | 3 ++-
 .../errorprone/bugpatterns/MethodCanBeStaticTest.java     | 8 ++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java b/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java
index db42998f2da..36236efbfc7 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/MethodCanBeStatic.java
@@ -24,6 +24,7 @@
 import static com.google.errorprone.util.ASTHelpers.getStartPosition;
 import static java.util.Collections.disjoint;
 import static javax.lang.model.element.Modifier.ABSTRACT;
+import static javax.lang.model.element.Modifier.DEFAULT;
 import static javax.lang.model.element.Modifier.NATIVE;
 import static javax.lang.model.element.Modifier.SYNCHRONIZED;
 
@@ -250,7 +251,7 @@ private static boolean isExcluded(MethodTree tree, VisitorState state) {
   }
 
   private static final ImmutableSet EXCLUDED_MODIFIERS =
-      immutableEnumSet(NATIVE, SYNCHRONIZED, ABSTRACT);
+      immutableEnumSet(NATIVE, SYNCHRONIZED, ABSTRACT, DEFAULT);
 
   /** Information about a {@link MethodSymbol} and whether it can be made static. */
   private static final class MethodDetails {
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java
index 330fe98d904..c06a3e6c69c 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/MethodCanBeStaticTest.java
@@ -484,4 +484,12 @@ public void abstractMethod_notFlagged() {
             "}")
         .doTest();
   }
+
+  @Test
+  public void defaultMethodExempted() {
+    testHelper
+        .addSourceLines(
+            "Test.java", "class Test {", "  private interface Foo { default void foo() {} }", "}")
+        .doTest();
+  }
 }

From 56e7e7755c8b4b32b9e3199331a947d129f0bdaa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89amonn=20McManus?= 
Date: Tue, 28 Jun 2022 11:18:10 -0700
Subject: [PATCH 046/102] Adjust to changes with JDK 19-ea.

* Enum case labels now have a different representation in the tree API.

* A 20-year-old [bug](https://bugs.openjdk.org/browse/JDK-4511638) that sometimes led to very long floating-point literals has been fixed.
  So our workaround for it is no longer needed, and we can no longer trigger it for tests.

RELNOTES=n/a
PiperOrigin-RevId: 457777421
---
 .../bugpatterns/WildcardImport.java           | 51 +++++++++++++++++--
 .../FloatingPointLiteralPrecisionTest.java    |  8 +++
 2 files changed, 54 insertions(+), 5 deletions(-)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java b/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java
index 763b20c0219..73d550b5127 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/WildcardImport.java
@@ -32,6 +32,7 @@
 import com.google.errorprone.util.ASTHelpers;
 import com.sun.source.tree.CaseTree;
 import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.IdentifierTree;
 import com.sun.source.tree.ImportTree;
 import com.sun.source.tree.MemberSelectTree;
@@ -43,10 +44,15 @@
 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
 import com.sun.tools.javac.tree.JCTree.JCIdent;
 import com.sun.tools.javac.tree.TreeScanner;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import javax.lang.model.element.ElementKind;
 
@@ -59,6 +65,7 @@
     tags = StandardTags.STYLE,
     link = "https://google.github.io/styleguide/javaguide.html?cl=head#s3.3.1-wildcard-imports")
 public class WildcardImport extends BugChecker implements CompilationUnitTreeMatcher {
+  private static final Logger logger = Logger.getLogger(WildcardImport.class.getName());
 
   /** Maximum number of members to import before switching to qualified names. */
   public static final int MAX_MEMBER_IMPORTS = 20;
@@ -213,6 +220,24 @@ static Fix createFix(
     return fix.build();
   }
 
+  private static final MethodHandle CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION;
+
+  static {
+    MethodHandle h;
+    try {
+      Class constantCaseLabelTree = Class.forName("com.sun.source.tree.ConstantCaseLabelTree");
+      h =
+          MethodHandles.lookup()
+              .findVirtual(
+                  constantCaseLabelTree,
+                  "getConstantExpression",
+                  MethodType.methodType(ExpressionTree.class));
+    } catch (ReflectiveOperationException e) {
+      h = null;
+    }
+    CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION = h;
+  }
+
   /**
    * Add an import for {@code owner}, and qualify all on demand imported references to members of
    * owner by owner's simple name.
@@ -228,11 +253,27 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) {
           return null;
         }
         Tree parent = getCurrentPath().getParentPath().getLeaf();
-        if (parent.getKind() == Tree.Kind.CASE
-            && ((CaseTree) parent).getExpression().equals(tree)
-            && sym.owner.getKind() == ElementKind.ENUM) {
-          // switch cases can refer to enum constants by simple name without importing them
-          return null;
+        if (sym.owner.getKind() == ElementKind.ENUM) {
+          if (parent.getKind() == Tree.Kind.CASE
+              && ((CaseTree) parent).getExpression().equals(tree)) {
+            // switch cases can refer to enum constants by simple name without importing them
+            return null;
+          }
+          // In JDK 19, the tree representation of enum case-labels changes. We can't reference the
+          // relevant API directly because then this code wouldn't compile on earlier JDK versions.
+          // So instead we use method handles. The straightforward code would be:
+          //   if (parent.getKind() == Tree.Kind.CONSTANT_CASE_LABEL
+          //       && tree.equals(((ConstantCaseLabelTree) parent).getConstantExpression())) {...}
+          if (parent.getKind().name().equals("CONSTANT_CASE_LABEL")) {
+            try {
+              if (tree.equals(CONSTANT_CASE_LABEL_TREE_GET_EXPRESSION.invoke(parent))) {
+                return null;
+              }
+            } catch (Throwable e) {
+              // MethodHandle.invoke obliges us to catch Throwable here.
+              logger.log(Level.SEVERE, "Could not compare trees", e);
+            }
+          }
         }
         if (sym.owner.equals(owner) && unit.starImportScope.includes(sym)) {
           fix.prefixWith(tree, owner.getSimpleName() + ".");
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java
index 9449e736134..a3b2ee2c4fb 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/FloatingPointLiteralPrecisionTest.java
@@ -17,6 +17,7 @@
 package com.google.errorprone.bugpatterns;
 
 import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.errorprone.BugCheckerRefactoringTestHelper;
 import com.google.errorprone.CompilationTestHelper;
@@ -70,6 +71,13 @@ public void negative() {
 
   @Test
   public void replacementTooLong() {
+    // In JDK versions before 19, String.valueOf(1e23) was 9.999999999999999E22, and the logic we're
+    // testing here was introduced to avoid introducing strings like that in rewrites. JDK 19 fixes
+    // https://bugs.openjdk.org/browse/JDK-4511638 (over 20 years after it was filed) so
+    // we don't need the logic or its test there.
+    String string1e23 = String.valueOf(1e23);
+    assumeTrue(string1e23.length() > "1e23".length() * 3);
+
     String[] input = {
       "class Test {", //
       "  // BUG: Diagnostic contains:",

From dc5a7b4b01f4f748962de35b70b660b89fb25864 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89amonn=20McManus?= 
Date: Tue, 28 Jun 2022 11:32:04 -0700
Subject: [PATCH 047/102] Make the default `@Nullable` for
 `ReturnMissingNullable` be the JSpecify one.

For now at least we are not doing this for Android because type-use annotations are not supported everywhere.

PiperOrigin-RevId: 457780634
---
 .../bugpatterns/nullness/NullnessUtils.java        |  7 ++++---
 .../nullness/FieldMissingNullableTest.java         |  2 +-
 .../nullness/ReturnMissingNullableTest.java        | 14 +++++++-------
 3 files changed, 12 insertions(+), 11 deletions(-)

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 1eee4bb056f..731165d88f1 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
@@ -35,6 +35,7 @@
 import static com.sun.source.tree.Tree.Kind.PARAMETERIZED_TYPE;
 import static com.sun.tools.javac.parser.Tokens.TokenKind.DOT;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.errorprone.ErrorProneFlags;
 import com.google.errorprone.VisitorState;
@@ -242,7 +243,7 @@ static boolean hasExtraParameterForEnclosingInstance(MethodSymbol symbol) {
     return enclosingClass(constructedClass) != null && !constructedClass.isStatic();
   }
 
-  @com.google.auto.value.AutoValue // fully qualified to work around JDK-7177813(?) in JDK8 build
+  @AutoValue
   abstract static class NullableAnnotationToUse {
     static NullableAnnotationToUse annotationToBeImported(String qualifiedName, boolean isTypeUse) {
       return new AutoValue_NullnessUtils_NullableAnnotationToUse(
@@ -334,7 +335,7 @@ private static NullableAnnotationToUse pickNullableAnnotation(VisitorState state
             .orElse(
                 state.isAndroidCompatible()
                     ? "androidx.annotation.Nullable"
-                    : "javax.annotation.Nullable");
+                    : "org.jspecify.nullness.Nullable");
     if (sym != null) {
       ClassSymbol classSym = (ClassSymbol) sym;
       if (classSym.isAnnotationType()) {
@@ -342,7 +343,7 @@ private static NullableAnnotationToUse pickNullableAnnotation(VisitorState state
         return annotationWithoutImporting(
             "Nullable", isTypeUse(classSym.className()), /*isAlreadyInScope=*/ true);
       } else {
-        // It's not an annotation type. We have to fully-qualify the import.
+        // The imported `Nullable` is not an annotation type. Fully qualify the annotation.
         return annotationWithoutImporting(
             defaultType, isTypeUse(defaultType), /*isAlreadyInScope=*/ false);
       }
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java
index a1277a3811a..da0a8bc8873 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/FieldMissingNullableTest.java
@@ -457,7 +457,7 @@ public void testNonAnnotationNullable() {
         .addOutputLines(
             "out/Test.java",
             "class T {",
-            "  @javax.annotation.Nullable private final Object obj2 = null;",
+            "  private final @org.jspecify.nullness.Nullable Object obj2 = null;",
             "  class Nullable {}",
             "}")
         .doTest();
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
index 440303b7b4c..f78105226f8 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
@@ -720,10 +720,10 @@ public void testRemoveSuppressWarnings_removeNullnessReturnWarning() {
         .addOutputLines(
             "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
             "package com.google.errorprone.bugpatterns.nullness;",
-            "import javax.annotation.Nullable;",
+            "import org.jspecify.nullness.Nullable;",
             "public class LiteralNullReturnTest {",
-            "  @Nullable ",
-            "  public String getMessage(boolean b) {",
+            "",
+            "  public @Nullable String getMessage(boolean b) {",
             "    if (b) {",
             "      return null;",
             "    } else {",
@@ -1541,8 +1541,8 @@ public void testNonAnnotationNullable() {
         .addOutputLines(
             "out/Test.java",
             "class T {",
-            "  @javax.annotation.Nullable private final Object method(boolean b) { return b ? null"
-                + " : 0; }",
+            "  @org.jspecify.nullness.Nullable private final Object method(boolean b) { return b ?"
+                + " null : 0; }",
             "  class Nullable {}",
             "}")
         .doTest();
@@ -1564,9 +1564,9 @@ public void testMultipleNullReturns() {
             "}")
         .addOutputLines(
             "out/Test.java",
-            "import javax.annotation.Nullable;",
+            "import org.jspecify.nullness.Nullable;",
             "class T {",
-            "  @Nullable private final Object method(boolean b) {",
+            "  private final @Nullable Object method(boolean b) {",
             "    if (b) {",
             "      return null;",
             "    } else {",

From 21de2da324db29530afc43d97f45b393a757a6b0 Mon Sep 17 00:00:00 2001
From: cpovirk 
Date: Tue, 28 Jun 2022 13:40:17 -0700
Subject: [PATCH 048/102] Handle switch expressions in `ReturnMissingNullable`
 and friends.

(Plain old switch _statements_ already worked, but I added tests to verify that they continue to work.)

PiperOrigin-RevId: 457809181
---
 .../bugpatterns/nullness/NullnessUtils.java   | 48 ++++++++++-
 .../nullness/ReturnMissingNullableTest.java   | 80 +++++++++++++++++++
 2 files changed, 126 insertions(+), 2 deletions(-)

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 731165d88f1..c81767bcc4c 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
@@ -34,6 +34,7 @@
 import static com.sun.source.tree.Tree.Kind.NULL_LITERAL;
 import static com.sun.source.tree.Tree.Kind.PARAMETERIZED_TYPE;
 import static com.sun.tools.javac.parser.Tokens.TokenKind.DOT;
+import static java.lang.Boolean.TRUE;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
@@ -51,6 +52,7 @@
 import com.sun.source.tree.AssignmentTree;
 import com.sun.source.tree.BinaryTree;
 import com.sun.source.tree.BlockTree;
+import com.sun.source.tree.CaseTree;
 import com.sun.source.tree.ConditionalExpressionTree;
 import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.IdentifierTree;
@@ -73,6 +75,9 @@
 import com.sun.tools.javac.code.Symbol.VarSymbol;
 import com.sun.tools.javac.code.Type;
 import com.sun.tools.javac.processing.JavacProcessingEnvironment;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import javax.annotation.Nullable;
 import javax.lang.model.element.Name;
@@ -505,7 +510,7 @@ public Boolean visitParenthesized(ParenthesizedTree tree, Void unused) {
         return visit(tree.getExpression(), unused);
       }
 
-      // TODO(cpovirk): visitSwitchExpression
+      // For visitSwitchExpression logic, see defaultAction.
 
       @Override
       public Boolean visitTypeCast(TypeCastTree tree, Void unused) {
@@ -521,7 +526,13 @@ protected Boolean defaultAction(Tree tree, Void unused) {
          * null)`.)
          */
         return isVoid(getType(tree), stateForCompilationUnit)
-            || definitelyNullVars.contains(getSymbol(tree));
+            || definitelyNullVars.contains(getSymbol(tree))
+            /*
+             * TODO(cpovirk): It would be nicer to report the finding on the null-returning `case`
+             * rather than on the `switch` as a whole. To do so, maybe we could change our visitor
+             * to accept `Boolean isCaseOfReturnedExpressionSwitch` instead of `Void unused`?
+             */
+            || isSwitchExpressionWithDefinitelyNullBranch(tree);
       }
 
       boolean isOptionalOrNull(MethodInvocationTree tree) {
@@ -533,9 +544,42 @@ boolean isOptionalOrNull(MethodInvocationTree tree) {
          * But consider whether that would interfere with the TODO at the top of that method.
          */
       }
+
+      boolean isSwitchExpressionWithDefinitelyNullBranch(Tree tree) {
+        return tree.getKind().name().equals("SWITCH_EXPRESSION")
+            && getCases(tree).stream()
+                .map(NullnessUtils::getBody)
+                .anyMatch(t -> Objects.equals(visit(t, null), TRUE));
+      }
     }.visit(tree, null);
   }
 
+  private static List getCases(Tree switchExpressionTree) {
+    try {
+      if (getCasesMethod == null) {
+        getCasesMethod =
+            Class.forName("com.sun.source.tree.SwitchExpressionTree").getMethod("getCases");
+      }
+      return (List) getCasesMethod.invoke(switchExpressionTree);
+    } catch (ReflectiveOperationException e) {
+      throw new LinkageError(e.getMessage(), e);
+    }
+  }
+
+  private static Tree getBody(Object caseTree) {
+    try {
+      if (getBodyMethod == null) {
+        getBodyMethod = CaseTree.class.getMethod("getBody");
+      }
+      return (Tree) getBodyMethod.invoke(caseTree);
+    } catch (ReflectiveOperationException e) {
+      throw new LinkageError(e.getMessage(), e);
+    }
+  }
+
+  private static Method getCasesMethod;
+  private static Method getBodyMethod;
+
   /** Returns true if this is {@code x == null ? x : ...} or similar. */
   private static boolean isTernaryXIfXIsNull(ConditionalExpressionTree tree) {
     NullCheck nullCheck = getNullCheck(tree.getCondition());
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
index f78105226f8..df2d5e4c575 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java
@@ -17,9 +17,11 @@
 package com.google.errorprone.bugpatterns.nullness;
 
 import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.errorprone.BugCheckerRefactoringTestHelper;
 import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.util.RuntimeVersion;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -135,6 +137,84 @@ public void testParenthesizedConditionalLiteralNullReturn() {
         .doTest();
   }
 
+  @Test
+  public void testSwitchExpressionTree() {
+    assumeTrue(RuntimeVersion.isAtLeast12());
+
+    createCompilationTestHelper()
+        .addSourceLines(
+            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
+            "package com.google.errorprone.bugpatterns.nullness;",
+            "public class LiteralNullReturnTest {",
+            "  public String getMessage(int x) {",
+            "    // BUG: Diagnostic contains: @Nullable",
+            "    return switch (x) {",
+            "      case 0 -> null;",
+            "      default -> \"non-zero\";",
+            "    };",
+            "  }",
+            "}")
+        .doTest();
+  }
+
+  @Test
+  public void testSwitchExpressionTree_negative() {
+    assumeTrue(RuntimeVersion.isAtLeast12());
+
+    createCompilationTestHelper()
+        .addSourceLines(
+            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
+            "package com.google.errorprone.bugpatterns.nullness;",
+            "public class LiteralNullReturnTest {",
+            "  public String getMessage(int x) {",
+            "    return switch (x) {",
+            "      case 0 -> \"zero\";",
+            "      default -> \"non-zero\";",
+            "    };",
+            "  }",
+            "}")
+        .doTest();
+  }
+
+  @Test
+  public void testSwitchStatement() {
+    createCompilationTestHelper()
+        .addSourceLines(
+            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
+            "package com.google.errorprone.bugpatterns.nullness;",
+            "public class LiteralNullReturnTest {",
+            "  public String getMessage(int x) {",
+            "    switch (x) {",
+            "      case 0:",
+            "        // BUG: Diagnostic contains: @Nullable",
+            "        return null;",
+            "      default:",
+            "        return \"non-zero\";",
+            "    }",
+            "  }",
+            "}")
+        .doTest();
+  }
+
+  @Test
+  public void testSwitchStatement_negative() {
+    createCompilationTestHelper()
+        .addSourceLines(
+            "com/google/errorprone/bugpatterns/nullness/LiteralNullReturnTest.java",
+            "package com.google.errorprone.bugpatterns.nullness;",
+            "public class LiteralNullReturnTest {",
+            "  public String getMessage(int x) {",
+            "    switch (x) {",
+            "      case 0:",
+            "        return \"zero\";",
+            "      default:",
+            "        return \"non-zero\";",
+            "    }",
+            "  }",
+            "}")
+        .doTest();
+  }
+
   @Test
   public void testVoidReturn() {
     createCompilationTestHelper()

From b312301accf487f1cb417a842640a176da49e6f0 Mon Sep 17 00:00:00 2001
From: ghm 
Date: Wed, 29 Jun 2022 04:07:19 -0700
Subject: [PATCH 049/102] Add some Markdown for AnnotationPosition.

PiperOrigin-RevId: 457940184
---
 docs/bugpattern/AnnotationPosition.md | 29 +++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)
 create mode 100644 docs/bugpattern/AnnotationPosition.md

diff --git a/docs/bugpattern/AnnotationPosition.md b/docs/bugpattern/AnnotationPosition.md
new file mode 100644
index 00000000000..9153a491a94
--- /dev/null
+++ b/docs/bugpattern/AnnotationPosition.md
@@ -0,0 +1,29 @@
+Per the [style guide](style-guide), `TYPE_USE` annotations should appear
+immediately before the type being annotated, and after any modifiers:
+
+```java
+public  @Nullable V getOrNull(final Map map, final @Nullable K key) {
+  return map.get(key);
+}
+```
+
+Non-`TYPE_USE` annotations should appear before modifiers, as they annotate the
+entire element (method, variable, class):
+
+```java
+@VisibleForTesting
+public void reset() {
+  // ...
+}
+```
+
+Javadoc must appear before any annotations, or the compiler will fail to
+recognise it as Javadoc:
+
+```java
+@Nullable
+/** Might return a frobnicator. */
+Frobnicator getFrobnicator();
+```
+
+[style-guide]: https://google.github.io/styleguide/javaguide.html#s4.8.5.1-type-use-annotation-style

From 69239660009fbb8d921f72f69226958f2f33ab3c Mon Sep 17 00:00:00 2001
From: ghm 
Date: Wed, 29 Jun 2022 07:20:47 -0700
Subject: [PATCH 050/102] Fix handling of verify in CannotMockFinalMethod.

The pattern is when(foo.method()) but verify(foo).method(). I clearly didn't have enough coffee before doing this.

PiperOrigin-RevId: 457970041
---
 .../bugpatterns/CannotMockFinalMethod.java    | 28 +++++++++++++------
 .../CannotMockFinalMethodTest.java            |  2 +-
 2 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java
index 80d935ae4eb..68c1a74bb10 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/CannotMockFinalMethod.java
@@ -17,6 +17,7 @@
 import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
 import static com.google.errorprone.matchers.Description.NO_MATCH;
 import static com.google.errorprone.matchers.Matchers.staticMethod;
+import static com.google.errorprone.util.ASTHelpers.getReceiver;
 import static com.google.errorprone.util.ASTHelpers.getSymbol;
 
 import com.google.errorprone.BugPattern;
@@ -27,6 +28,7 @@
 import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.MethodInvocationTree;
 import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
 
 /** A BugPattern; see the summary */
 @BugPattern(
@@ -34,19 +36,29 @@
     severity = WARNING)
 public final class CannotMockFinalMethod extends BugChecker implements MethodInvocationTreeMatcher {
 
-  private static final Matcher VERIFY_WHEN =
-      staticMethod().onClass("org.mockito.Mockito").namedAnyOf("verify", "when");
+  private static final Matcher WHEN =
+      staticMethod().onClass("org.mockito.Mockito").named("when");
+
+  private static final Matcher VERIFY =
+      staticMethod().onClass("org.mockito.Mockito").named("verify");
 
   @Override
   public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
-    if (!VERIFY_WHEN.matches(tree, state)) {
-      return NO_MATCH;
+    if (WHEN.matches(tree, state)) {
+      ExpressionTree firstArgument = tree.getArguments().get(0);
+      if (!(firstArgument instanceof MethodInvocationTree)) {
+        return NO_MATCH;
+      }
+      return describe(tree, getSymbol((MethodInvocationTree) firstArgument));
     }
-    ExpressionTree firstArgument = tree.getArguments().get(0);
-    if (!(firstArgument instanceof MethodInvocationTree)) {
-      return NO_MATCH;
+    var receiver = getReceiver(tree);
+    if (receiver != null && VERIFY.matches(receiver, state)) {
+      return describe(tree, getSymbol(tree));
     }
-    var methodSymbol = getSymbol((MethodInvocationTree) firstArgument);
+    return NO_MATCH;
+  }
+
+  private Description describe(MethodInvocationTree tree, MethodSymbol methodSymbol) {
     return (methodSymbol.flags() & Flags.FINAL) == 0 ? NO_MATCH : describeMatch(tree);
   }
 }
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java
index aba94d7c324..86f5379f4b2 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/CannotMockFinalMethodTest.java
@@ -56,7 +56,7 @@ public void verifyCall_flagged() {
             "  }",
             "  void test() {",
             "    // BUG: Diagnostic contains:",
-            "    verify(this.foo());",
+            "    verify(this).foo();",
             "  }",
             "}")
         .doTest();

From 43c41b61e6d054c0abdb329308551443624cbbb9 Mon Sep 17 00:00:00 2001
From: Alan Malloy 
Date: Wed, 29 Jun 2022 12:36:50 -0700
Subject: [PATCH 051/102] Migrate callers of descending() to ascending()

PiperOrigin-RevId: 458039060
---
 .../apply/DescriptionBasedDiff.java           |  5 +-
 .../google/errorprone/apply/SourceFile.java   | 53 +++++++++++++++++++
 .../google/errorprone/fixes/Replacements.java |  9 ++--
 .../google/errorprone/fixes/SuggestedFix.java |  2 +-
 .../errorprone/fixes/ReplacementsTest.java    | 10 ++--
 5 files changed, 66 insertions(+), 13 deletions(-)

diff --git a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
index 0e704ba6040..3df86f4abdd 100644
--- a/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
+++ b/check_api/src/main/java/com/google/errorprone/apply/DescriptionBasedDiff.java
@@ -123,9 +123,6 @@ public void applyDifferences(SourceFile sourceFile) throws DiffNotApplicableExce
             Replacements.CoalescePolicy.REPLACEMENT_FIRST);
       }
     }
-    for (Replacement replacement : replacements.descending()) {
-      sourceFile.replaceChars(
-          replacement.startPosition(), replacement.endPosition(), replacement.replaceWith());
-    }
+    sourceFile.makeReplacements(replacements);
   }
 }
diff --git a/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java b/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
index 5f76a929c83..b5a6d69dd0e 100644
--- a/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
+++ b/check_api/src/main/java/com/google/errorprone/apply/SourceFile.java
@@ -16,9 +16,15 @@
 
 package com.google.errorprone.apply;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.io.CharSource;
+import com.google.errorprone.fixes.Replacement;
+import com.google.errorprone.fixes.Replacements;
 import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.StringReader;
@@ -158,4 +164,51 @@ public void replaceChars(int startPosition, int endPosition, String replacement)
               path, sourceBuilder.length(), startPosition, endPosition, replacement));
     }
   }
+
+  void makeReplacements(Replacements changes) {
+    ImmutableSet replacements = changes.ascending();
+    switch (replacements.size()) {
+      case 0:
+        return;
+      case 1:
+        {
+          Replacement onlyReplacement = Iterables.getOnlyElement(replacements);
+          replaceChars(
+              onlyReplacement.startPosition(),
+              onlyReplacement.endPosition(),
+              onlyReplacement.replaceWith());
+          return;
+        }
+      default:
+        break;
+    }
+
+    // Since we have many replacements to make all at once, it's better to start off with a clean
+    // slate, rather than make multiple separate replacements which each require shifting around
+    // the tail of our sourceBuilder. If we do them all at once, we can work forward from the
+    // beginning of the tile, so that each new replacement does not affect any previous
+    // replacements.
+    StringBuilder newContent = new StringBuilder();
+    int positionInOriginal = 0;
+    for (Replacement repl : replacements) {
+      checkArgument(
+          repl.endPosition() <= sourceBuilder.length(),
+          "End [%s] should not exceed source length [%s]",
+          repl.endPosition(),
+          sourceBuilder.length());
+
+      // Write the unmodified content leading up to this change
+      newContent.append(sourceBuilder, positionInOriginal, repl.startPosition());
+      // And the modified content for this change
+      newContent.append(repl.replaceWith());
+      // Then skip everything from source between start and end
+      positionInOriginal = repl.endPosition();
+    }
+    // Flush out any remaining content after the final change
+    newContent.append(sourceBuilder, positionInOriginal, sourceBuilder.length());
+    // Overwrite the contents of our old buffer. Note we mutate the existing StringBuilder rather
+    // than replacing it, because other clients may have a view of the content via getAsSequence,
+    // and we want that view to reflect the new content.
+    setSourceText(newContent);
+  }
 }
diff --git a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
index b3ff921353f..a7af506ea9f 100644
--- a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
+++ b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java
@@ -143,7 +143,12 @@ private void checkOverlaps(Replacement replacement) {
     }
   }
 
-  /** Non-overlapping replacements, sorted in descending order by position. */
+  /**
+   * Non-overlapping replacements, sorted in descending order by position. Prefer using {@link
+   * #ascending} when applying changes, because applying changes in reverse tends to result in
+   * quadratic-time copying of the underlying string.
+   */
+  @Deprecated
   public Set descending() {
     // TODO(cushon): refactor SuggestedFix#getReplacements and just return a Collection,
     return new LinkedHashSet<>(replacements.values());
@@ -151,8 +156,6 @@ public Set descending() {
 
   /** Non-overlapping replacements, sorted in ascending order by position. */
   public ImmutableSet ascending() {
-    // TODO(amalloy): Encourage using this instead of descending()
-    // Applying replacements in forward order is substantially more efficient, and only a bit harder
     return ImmutableSet.copyOf(replacements.descendingMap().values());
   }
 
diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
index 9b280f364c5..e51a1dc4f41 100644
--- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
+++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java
@@ -94,7 +94,7 @@ public Set getReplacements(EndPosTable endPositions) {
       replacements.add(
           fix.getReplacement(endPositions), Replacements.CoalescePolicy.EXISTING_FIRST);
     }
-    return replacements.descending();
+    return replacements.ascending();
   }
 
   /** {@link Builder#replace(Tree, String)} */
diff --git a/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java b/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java
index 3117d66533d..8ef3a0901a6 100644
--- a/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java
+++ b/check_api/src/test/java/com/google/errorprone/fixes/ReplacementsTest.java
@@ -69,24 +69,24 @@ public Range apply(Replacement replacement) {
       };
 
   @Test
-  public void descending() {
+  public void ascending() {
     assertThat(
             Iterables.transform(
                 new Replacements()
                     .add(Replacement.create(0, 0, "hello"))
                     .add(Replacement.create(0, 1, "hello"))
-                    .descending(),
+                    .ascending(),
                 AS_RANGES))
-        .containsExactly(Range.closedOpen(0, 1), Range.closedOpen(0, 0))
+        .containsExactly(Range.closedOpen(0, 0), Range.closedOpen(0, 1))
         .inOrder();
     assertThat(
             Iterables.transform(
                 new Replacements()
                     .add(Replacement.create(0, 1, "hello"))
                     .add(Replacement.create(0, 0, "hello"))
-                    .descending(),
+                    .ascending(),
                 AS_RANGES))
-        .containsExactly(Range.closedOpen(0, 1), Range.closedOpen(0, 0))
+        .containsExactly(Range.closedOpen(0, 0), Range.closedOpen(0, 1))
         .inOrder();
   }
 

From bf86f1f5ce01dd51c5928c75d3b73bca38d611c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89amonn=20McManus?= 
Date: Wed, 29 Jun 2022 13:20:44 -0700
Subject: [PATCH 052/102] Add an `explanation` to `ReturnMissingNullable`.

PiperOrigin-RevId: 458048240
---
 .../bugpatterns/nullness/ReturnMissingNullable.java       | 8 ++++++++
 1 file changed, 8 insertions(+)

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 861fb26e260..bb557cc181f 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
@@ -74,6 +74,14 @@
 /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
 @BugPattern(
     summary = "Method returns a definitely null value but is not annotated @Nullable",
+    explanation =
+        "Annotating a method @Nullable communicates to tools that the method can return null. That"
+            + " means they can check that callers handle a returned null correctly.\n\n"
+            + "Adding @Nullable may require updating callers so that they deal with the"
+            + " possibly-null value. This can happen for example with Kotlin callers, or with Java"
+            + " callers that are checked for null-safety by static-analysis tools. Alternatively,"
+            + " depending on the tool, it may be possible to annotate Java callers temporarily with"
+            + " @SuppressWarnings(\"nullness\").",
     severity = SUGGESTION)
 public class ReturnMissingNullable extends BugChecker implements CompilationUnitTreeMatcher {
   private static final Matcher METHODS_THAT_NEVER_RETURN =

From d1026b1fe1326d87d1a8496b652585441bcd6861 Mon Sep 17 00:00:00 2001
From: Alan Malloy 
Date: Wed, 29 Jun 2022 15:58:52 -0700
Subject: [PATCH 053/102] Don't consider line comments to be ParameterName
 comments

Users do this accidentally as often as they do on purpose.

PiperOrigin-RevId: 458084069
---
 .../errorprone/bugpatterns/ParameterName.java |  6 +++
 .../bugpatterns/ParameterNameTest.java        | 49 +++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
index 0b18a605a70..80e12e376b3 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ParameterName.java
@@ -46,6 +46,7 @@
 import com.sun.tools.javac.code.Symbol.MethodSymbol;
 import com.sun.tools.javac.code.Symbol.VarSymbol;
 import com.sun.tools.javac.parser.Tokens.Comment;
+import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle;
 import com.sun.tools.javac.util.Position;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -199,6 +200,11 @@ private void checkArgument(
       VarSymbol formal, ExpressionTree actual, ErrorProneToken token, VisitorState state) {
     List matches = new ArrayList<>();
     for (Comment comment : token.comments()) {
+      if (comment.getStyle().equals(CommentStyle.LINE)) {
+        // These are usually not intended as a parameter comment, and we don't want to flag if they
+        // happen to match the parameter comment format.
+        continue;
+      }
       Matcher m =
           NamedParameterComment.PARAMETER_COMMENT_PATTERN.matcher(
               Comments.getTextFromComment(comment));
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java
index 92e8ca467ee..3769df4c379 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/ParameterNameTest.java
@@ -269,6 +269,39 @@ public void namedParametersChecker_toleratesMatchingComment_lineAfter() {
         .doTest();
   }
 
+  /**
+   * We allow multiple comments if any one of them is right. This helps libraries migrate to a new
+   * parameter name gradually without breaking any builds: update the caller to use both names, then
+   * update the API, then remove the old name.
+   */
+  @Test
+  public void namedParametersChecker_multipleComments_allowedIfAnyMatch() {
+    testHelper
+        .addSourceLines(
+            "Test.java",
+            "class Test {",
+            "  void test(Object x) {",
+            "    test(/* y= */ /* x= */ x);",
+            "    test(/* x= */ /* y= */ x);",
+            "  }",
+            "}")
+        .doTest();
+  }
+
+  @Test
+  public void namedParametersChecker_multipleComments_flaggedIfNoneMatch() {
+    testHelper
+        .addSourceLines(
+            "Test.java",
+            "class Test {",
+            "  void test(Object x) {",
+            "    // BUG: Diagnostic contains: does not match",
+            "    test(/* y= */ /* z= */ x);",
+            "  }",
+            "}")
+        .doTest();
+  }
+
   @Test
   public void namedParametersChecker_ignoresComment_nonMatchinglineAfter() {
     testHelper
@@ -299,6 +332,22 @@ public void namedParametersChecker_ignoresComment_markedUpDelimiter() {
         .doTest();
   }
 
+  @Test
+  public void namedParametersChecker_ignoresLineComments() {
+    testHelper
+        .addSourceLines(
+            "Test.java",
+            "class Test {",
+            "  void test(int x) {",
+            "    test(",
+            "      // newX =",
+            "      //   (x ^ 2)",
+            "      x * x);",
+            "  }",
+            "}")
+        .doTest();
+  }
+
   @Test
   public void namedParametersChecker_ignoresComment_wrongNameWithNoEquals() {
     testHelper

From 65bf2ab3fec0830a6fdf4fdf4561647a257ed6fe Mon Sep 17 00:00:00 2001
From: ghm 
Date: Thu, 30 Jun 2022 09:02:19 -0700
Subject: [PATCH 054/102] Ignore methods which have had `thenCallRealMethod`
 used on them.

PiperOrigin-RevId: 458238873
---
 .../bugpatterns/DirectInvocationOnMock.java   | 24 +++++++++++++++++++
 .../DirectInvocationOnMockTest.java           | 17 +++++++++++++
 2 files changed, 41 insertions(+)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java
index ca525f7fc9e..26ce3ea9df5 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java
@@ -21,6 +21,7 @@
 import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport;
 import static com.google.errorprone.matchers.Description.NO_MATCH;
 import static com.google.errorprone.matchers.Matchers.anyMethod;
+import static com.google.errorprone.matchers.Matchers.instanceMethod;
 import static com.google.errorprone.matchers.Matchers.staticMethod;
 import static com.google.errorprone.util.ASTHelpers.getReceiver;
 import static com.google.errorprone.util.ASTHelpers.getSymbol;
@@ -44,7 +45,10 @@
 import com.sun.source.tree.VariableTree;
 import com.sun.source.util.TreeScanner;
 import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
 import com.sun.tools.javac.code.Symbol.VarSymbol;
+import java.util.HashSet;
+import java.util.Set;
 
 /** A bugpattern; see the description. */
 @BugPattern(
@@ -56,10 +60,25 @@ public final class DirectInvocationOnMock extends BugChecker implements Compilat
   @Override
   public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
     ImmutableSet mocks = findMocks(state);
+    Set methodsCallingRealImplementations = new HashSet<>();
 
     new SuppressibleTreePathScanner(state) {
       @Override
       public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
+        if (THEN_CALL_REAL_METHOD.matches(tree, state)) {
+          var receiver = getReceiver(tree);
+          if (receiver != null && WHEN.matches(receiver, state)) {
+            ExpressionTree firstArgument = ((MethodInvocationTree) receiver).getArguments().get(0);
+            var firstArgumentSymbol = getSymbol(firstArgument);
+            if (firstArgumentSymbol instanceof MethodSymbol) {
+              methodsCallingRealImplementations.add((MethodSymbol) firstArgumentSymbol);
+            }
+          }
+          return super.visitMethodInvocation(tree, null);
+        }
+        if (methodsCallingRealImplementations.contains(getSymbol(tree))) {
+          return super.visitMethodInvocation(tree, null);
+        }
         if ((getSymbol(tree).flags() & Flags.FINAL) != 0) {
           return null;
         }
@@ -136,4 +155,9 @@ public Void visitAssignment(AssignmentTree tree, Void unused) {
       staticMethod().onClass("org.mockito.Mockito").named("mock").withParameters("java.lang.Class");
 
   private static final Matcher WHEN = anyMethod().anyClass().named("when");
+
+  private static final Matcher THEN_CALL_REAL_METHOD =
+      instanceMethod()
+          .onDescendantOf("org.mockito.stubbing.OngoingStubbing")
+          .named("thenCallRealMethod");
 }
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java
index fc1772d616b..1fe2822797e 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java
@@ -155,6 +155,23 @@ public void directInvocationOnMock_withinWhen_noFinding() {
         .doTest();
   }
 
+  @Test
+  public void directInvocationOnMock_setUpToCallRealMethod_noFinding() {
+    helper
+        .addSourceLines(
+            "Test.java",
+            "import static org.mockito.Mockito.mock;",
+            "import static org.mockito.Mockito.when;",
+            "class Test {",
+            "  public Object test() {",
+            "    Test test = mock(Test.class);",
+            "    when(test.test()).thenCallRealMethod();",
+            "    return test.test();",
+            "  }",
+            "}")
+        .doTest();
+  }
+
   @Test
   public void directInvocationOnMock_withinCustomWhen_noFinding() {
     helper

From 44d28f4626dfa743d9f2177254f8287f758b363e Mon Sep 17 00:00:00 2001
From: Chaoren Lin 
Date: Fri, 1 Jul 2022 10:45:57 -0700
Subject: [PATCH 055/102] Complete MustBeClosed porting.

- Implement the Java parts of the check based on []
- Add suggested fixes for Kotlin where reasonable.

The Java test cases are forked from Error Prone.

PiperOrigin-RevId: 458501281
---
 .../MustBeClosedCheckerNegativeCases.java     |  9 ++++
 .../MustBeClosedCheckerPositiveCases.java     | 35 ++++++++++++++-
 ...BeClosedCheckerPositiveCases_expected.java | 43 ++++++++++++++++++-
 3 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java
index c22f8539064..d5b66520761 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerNegativeCases.java
@@ -80,6 +80,11 @@ Closeable positiveCase7() {
     return new Foo().mustBeClosedAnnotatedMethod();
   }
 
+  @MustBeClosed
+  Closeable ternary(boolean condition) {
+    return condition ? new Foo().mustBeClosedAnnotatedMethod() : null;
+  }
+
   void tryWithResources() {
     Foo foo = new Foo();
     Closeable closeable = foo.mustBeClosedAnnotatedMethod();
@@ -138,4 +143,8 @@ void statementLambdaReturningCloseable() {
           return new MustBeClosedAnnotatedConstructor();
         });
   }
+
+  void methodReferenceReturningCloseable() {
+    consumeCloseable(MustBeClosedAnnotatedConstructor::new);
+  }
 }
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java
index e25fb620534..627809e0095 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases.java
@@ -19,6 +19,7 @@
 import com.google.errorprone.annotations.MustBeClosed;
 import java.util.stream.Stream;
 
+@SuppressWarnings({"UnusedNestedClass", "UnusedVariable"})
 class MustBeClosedCheckerPositiveCases {
 
   class DoesNotImplementAutoCloseable {
@@ -112,6 +113,7 @@ Closeable positiveCase7() {
   }
 
   void positiveCase8() {
+    // Lambda has a fixless finding because no reasonable fix can be suggested
     Lambda expression =
         () -> {
           // BUG: Diagnostic contains:
@@ -120,6 +122,11 @@ void positiveCase8() {
   }
 
   void positiveCase9() {
+    // TODO(b/218377318): BUG: Diagnostic contains:
+    Lambda expression = new Foo()::mustBeClosedAnnotatedMethod;
+  }
+
+  void positiveCase10() {
     new Foo() {
       @Override
       public Closeable mustBeClosedAnnotatedMethod() {
@@ -129,12 +136,38 @@ public Closeable mustBeClosedAnnotatedMethod() {
     };
   }
 
-  int expressionDeclaredVariable() {
+  void subexpression() {
+    // BUG: Diagnostic contains:
+    new Foo().mustBeClosedAnnotatedMethod().method();
+  }
+
+  void ternary(boolean condition) {
+    // BUG: Diagnostic contains:
+    int result = condition ? new Foo().mustBeClosedAnnotatedMethod().method() : 0;
+  }
+
+  int variableDeclaration() {
     // BUG: Diagnostic contains:
     int result = new Foo().mustBeClosedAnnotatedMethod().method();
     return result;
   }
 
+  void forLoopInitialization() {
+    // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains:
+    // for (int i = new Foo().mustBeClosedAnnotatedMethod().method(); i > 0; --i) { }
+  }
+
+  void forLoopConditionUnfixable() {
+    // TODO(b/236715080): suggested fix changes behavior.
+    // BUG: Diagnostic contains:
+    for (int i = 0; i < new Foo().mustBeClosedAnnotatedMethod().method(); ++i) {}
+  }
+
+  void forLoopUpdateUnfixable() {
+    // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains:
+    // for (int i = 0; i < 100; i += new Foo().mustBeClosedAnnotatedMethod().method()) {}
+  }
+
   void tryWithResources_nonFinal() {
     Foo foo = new Foo();
     // BUG: Diagnostic contains:
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java
index 85da29e0a84..171d444587a 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/MustBeClosedCheckerPositiveCases_expected.java
@@ -19,10 +19,12 @@
 import com.google.errorprone.annotations.MustBeClosed;
 import java.util.stream.Stream;
 
+@SuppressWarnings({"UnusedNestedClass", "UnusedVariable"})
 class MustBeClosedCheckerPositiveCases {
 
   class DoesNotImplementAutoCloseable {
     @MustBeClosed
+    // BUG: Diagnostic contains: MustBeClosed should only annotate constructors of AutoCloseables.
     DoesNotImplementAutoCloseable() {}
 
     @MustBeClosed
@@ -124,6 +126,11 @@ void positiveCase8() {
   }
 
   void positiveCase9() {
+    // TODO(b/218377318): BUG: Diagnostic contains:
+    Lambda expression = new Foo()::mustBeClosedAnnotatedMethod;
+  }
+
+  void positiveCase10() {
     new Foo() {
       @MustBeClosed
       @Override
@@ -134,7 +141,23 @@ public Closeable mustBeClosedAnnotatedMethod() {
     };
   }
 
-  int expressionDeclaredVariable() {
+  void subexpression() {
+    // BUG: Diagnostic contains:
+    try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) {
+      closeable.method();
+    }
+  }
+
+  void ternary(boolean condition) {
+    // BUG: Diagnostic contains:
+    int result;
+    try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) {
+      result = condition ? closeable.method() : 0;
+    }
+  }
+
+  int variableDeclaration() {
+    // BUG: Diagnostic contains:
     int result;
     try (Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) {
       result = closeable.method();
@@ -142,6 +165,24 @@ int expressionDeclaredVariable() {
     return result;
   }
 
+  void forLoopInitialization() {
+    // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains:
+    // for (int i = new Foo().mustBeClosedAnnotatedMethod().method(); i > 0; --i) {}
+  }
+
+  void forLoopConditionUnfixable() {
+    // TODO(b/236715080): suggested fix changes behavior.
+    // BUG: Diagnostic contains:
+    try (final Closeable closeable = new Foo().mustBeClosedAnnotatedMethod()) {
+      for (int i = 0; i < closeable.method(); ++i) {}
+    }
+  }
+
+  void forLoopUpdateUnfixable() {
+    // TODO(b/236715080): fix results in invalid code. BUG: Diagnostic contains:
+    // for (int i = 0; i < 100; i += new Foo().mustBeClosedAnnotatedMethod().method()) {}
+  }
+
   void tryWithResources_nonFinal() {
     Foo foo = new Foo();
     // BUG: Diagnostic contains:

From abf2681c5ee0583cfc6b1747c713e09435bf81e8 Mon Sep 17 00:00:00 2001
From: Nick Glorioso 
Date: Fri, 1 Jul 2022 13:08:46 -0700
Subject: [PATCH 056/102] Enhance the CanIgnoreReturnValueSuggester to look for
 calls to methods that are already @CanIgnoreReturnValue, to allow detecting
 when builder methods chain to one-another.

NOTE: Based on the tests, it looks like a method that is just `return self();` shouldn't also be considered CIRV for the same reason as `return this;`

Flume CL is pending now, but the tests pass

PiperOrigin-RevId: 458529607
---
 .../CanIgnoreReturnValueSuggester.java        | 203 ++++++++++--------
 .../CanIgnoreReturnValueSuggesterTest.java    |  44 ++++
 2 files changed, 157 insertions(+), 90 deletions(-)

diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java
index 5379dfb2685..64688ff45ca 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java
@@ -31,23 +31,23 @@
 import com.google.errorprone.fixes.SuggestedFix;
 import com.google.errorprone.matchers.Description;
 import com.google.errorprone.suppliers.Supplier;
+import com.google.errorprone.util.ASTHelpers;
 import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.IdentifierTree;
 import com.sun.source.tree.LambdaExpressionTree;
+import com.sun.source.tree.MemberSelectTree;
 import com.sun.source.tree.MethodInvocationTree;
 import com.sun.source.tree.MethodTree;
 import com.sun.source.tree.NewClassTree;
 import com.sun.source.tree.ReturnTree;
 import com.sun.source.tree.StatementTree;
-import com.sun.source.tree.Tree;
-import com.sun.source.util.TreePathScanner;
+import com.sun.source.util.TreeScanner;
 import com.sun.tools.javac.code.Symbol.MethodSymbol;
 import com.sun.tools.javac.code.Type;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Checker that recommends annotating a method with {@code @CanIgnoreReturnValue} if the method
- * always returns {@code this}.
+ * returns {@code this} (or other methods that are likely to also just return {@code this}).
  */
 @BugPattern(
     summary = "Methods that always 'return this' should be annotated with @CanIgnoreReturnValue",
@@ -61,88 +61,139 @@ public final class CanIgnoreReturnValueSuggester extends BugChecker implements M
 
   // TODO(kak): catch places where an input parameter is always returned
 
-  // TODO(b/202772719): catch cases where a method delegates to a CIRV method. E.g.:
-  //
-  //   public Builder addFoos(Foo... foos) {
-  //     return addFoos(asList(foos));
-  //   }
-  //   @CanIgnoreReturnValue
-  //   public Builder addFoos(Iterable foos) {
-  //     this.foos = checkNotNull(foos);
-  //     return this;
-  //   }
-  //
-  // This would also catch the `return self();` pattern and `return thisFoo();` patterns that we
-  // sometimes see (b/202772578#comment6 and b/202772578#comment7).
-  // All of this would work better if we had multi-pass, otherwise newly annotated @CIRV methods
-  // won't "propagate" until the next analysis pass.
-
   @Override
   public Description matchMethod(MethodTree methodTree, VisitorState state) {
     MethodSymbol methodSymbol = getSymbol(methodTree);
-    // if the method is static or doesn't have a body, it can't possibly "return this", bail out
-    if (methodSymbol.isStatic() || methodTree.getBody() == null) {
+
+    // We have a number of preconditions we can check early to ensure that this method could
+    // possibly be @CIRV-suggestible, before attempting a deeper scan of the method.
+    if (methodSymbol.isStatic()
+        || methodTree.getBody() == null
+        // These methods should probably be @CheckReturnValue!
+        || isDefinitionOfZeroArgSelf(methodSymbol)
+        // Constructors can't "return", and generally shouldn't be @CIRV
+        || methodTree.getReturnType() == null
+        // methods whose return type is void or Void can't have @CIRV
+        || isVoidType(getType(methodTree.getReturnType()), state)
+        // b/236423646 - These methods that do nothing *but* `return this;` are likely to be
+        // overridden in other contexts, and we've decided that these methods shouldn't be annotated
+        // automatically.
+        || isSimpleReturnThisMethod(methodTree)
+        // TODO(kak): This appears to be a performance optimization for refactoring passes?
+        || isSubtype(methodSymbol.owner.type, PROTO_BUILDER.get(state), state)
+        || hasAnnotation(methodSymbol, CIRV, state)) {
       return Description.NO_MATCH;
     }
 
-    // if the method is a proto builder method, bail out
-    if (isProtoBuilderSubtype(methodSymbol.owner.type, state)) {
-      // TODO(kak): could we just use this instead?
-      // ProtoRules.protoBuilders().evaluate(methodSymbol, state).isPresent()
+    // if the enclosing type is already annotated with CIRV, we could theoretically _not_ directly
+    // annotate the method but we're likely to discourage annotating types with CIRV: b/229776283
+    // if the method is already directly annotated w/ @CRV, bail out
+    if (hasAnnotation(methodTree, CRV, state)) {
+      // TODO(kak): we might want to actually _remove_ @CRV and add @CIRV in this case!
       return Description.NO_MATCH;
     }
 
-    if (methodSymbol.getSimpleName().contentEquals("self")
-        && methodSymbol.getParameters().isEmpty()) {
-      // would we want to actually suggest @CheckReturnValue on a self() method???
+    // OK, now the real implementation: For each possible return branch, does the expression
+    // returned look like "this" or instance methods that are also @CanIgnoreReturnValue.
+    if (!methodReturnsIgnorableValues(methodTree, state)) {
       return Description.NO_MATCH;
     }
 
-    // if the method doesn't do anything but "return this", bail out; see b/236423646
+    SuggestedFix.Builder fix = SuggestedFix.builder();
+    String cirvName = qualifyType(state, fix, CIRV);
+    // we could add a trailing comment (e.g., @CanIgnoreReturnValue // returns `this`), but all
+    // developers will become familiar with these annotations sooner or later
+    fix.prefixWith(methodTree, "@" + cirvName + "\n");
+
+    return describeMatch(methodTree, fix.build());
+  }
+
+  private static boolean isSimpleReturnThisMethod(MethodTree methodTree) {
     if (methodTree.getBody().getStatements().size() == 1) {
       StatementTree onlyStatement = methodTree.getBody().getStatements().get(0);
       if (onlyStatement instanceof ReturnTree) {
-        if (returnsThis((ReturnTree) onlyStatement, state)) {
-          return Description.NO_MATCH;
-        }
+        return returnsThisOrSelf((ReturnTree) onlyStatement);
       }
     }
+    return false;
+  }
 
-    // if the method is already directly annotated w/ @CIRV, bail out
-    if (hasAnnotation(methodTree, CIRV, state)) {
-      return Description.NO_MATCH;
+  private static boolean isIdentifier(ExpressionTree expr, String identifierName) {
+    expr = ASTHelpers.stripParentheses(expr);
+    if (expr instanceof IdentifierTree) {
+      return ((IdentifierTree) expr).getName().contentEquals(identifierName);
     }
+    return false;
+  }
 
-    // if the enclosing type is already annotated with CIRV, we could theoretically _not_ directly
-    // annotate the method but we're likely to discourage annotating types with CIRV: b/229776283
+  /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */
+  private static boolean returnsThisOrSelf(ReturnTree returnTree) {
+    return isIdentifier(returnTree.getExpression(), "this")
+        || (returnTree.getExpression() instanceof MethodInvocationTree
+            && isCallToZeroArgSelf((MethodInvocationTree) returnTree.getExpression()));
+  }
 
-    // if the method is already directly annotated w/ @CRV, bail out
-    if (hasAnnotation(methodTree, CRV, state)) {
-      // TODO(kak): we might want to actually _remove_ @CRV and add @CIRV in this case!
-      return Description.NO_MATCH;
+  // this.self() or self()
+  private static boolean isCallToZeroArgSelf(MethodInvocationTree mit) {
+    if (!mit.getArguments().isEmpty()) {
+      return false;
     }
-
-    // if the method is a constructor or has a void/Void return type, bail out
-    Tree returnType = methodTree.getReturnType();
-    if (returnType == null /* constructors have a null returnType */
-        || isVoidType(getType(returnType), state)) {
-      // TODO(b/234176673): isVoidType() also flags the `Void` (capital) type; is that desired?
-      return Description.NO_MATCH;
+    if (isIdentifier(mit.getMethodSelect(), "self")) {
+      return true;
+    }
+    if (mit.getMethodSelect() instanceof MemberSelectTree) {
+      MemberSelectTree methodSelect = (MemberSelectTree) mit.getMethodSelect();
+      return isIdentifier(methodSelect.getExpression(), "this")
+          && methodSelect.getIdentifier().contentEquals("self");
     }
+    return false;
+  }
+
+  private static boolean isDefinitionOfZeroArgSelf(MethodSymbol methodSymbol) {
+    return methodSymbol.getSimpleName().contentEquals("self")
+        && methodSymbol.getParameters().isEmpty();
+  }
 
-    AtomicBoolean allReturnThis = new AtomicBoolean(true);
-    AtomicBoolean atLeastOneReturn = new AtomicBoolean(false);
+  private static boolean methodReturnsIgnorableValues(MethodTree tree, VisitorState state) {
+    class ReturnValuesFromMethodAreIgnorable extends TreeScanner {
+      private final VisitorState state;
+      private MethodSymbol symbol;
+      private boolean atLeastOneReturn = false;
+      private boolean allReturnsIgnorable = true;
+
+      private ReturnValuesFromMethodAreIgnorable(VisitorState state, MethodSymbol methSymbol) {
+        this.state = state;
+        this.symbol = methSymbol;
+      }
 
-    new TreePathScanner() {
       @Override
       public Void visitReturn(ReturnTree returnTree, Void unused) {
-        atLeastOneReturn.set(true);
-        if (!returnsThis(returnTree, state)) {
-          allReturnThis.set(false);
-          // once we've set allReturnThis to false, no need to descend further
-          return null;
+        atLeastOneReturn = true;
+        if (!returnsThisOrSelf(returnTree)
+            && !isIgnorableMethodCallOnSameInstance(returnTree, state)) {
+          allReturnsIgnorable = false;
+        }
+        // Don't descend deeper into returns, since we already checked the body of this return.
+        return null;
+      }
+
+      private boolean isIgnorableMethodCallOnSameInstance(
+          ReturnTree returnTree, VisitorState state) {
+        if (returnTree.getExpression() instanceof MethodInvocationTree) {
+          MethodInvocationTree mit = (MethodInvocationTree) returnTree.getExpression();
+          ExpressionTree receiver = ASTHelpers.getReceiver(mit);
+          MethodSymbol calledMethod = getSymbol(mit);
+          if ((receiver == null && !calledMethod.isStatic())
+              || isIdentifier(receiver, "this")
+              || isIdentifier(receiver, "super")) {
+            // If the method we're calling is @CIRV and returns a subtype of the enclosing symbol,
+            // then we think it's likely ~this.
+            return ASTHelpers.hasAnnotation(calledMethod, CIRV, state)
+                && ASTHelpers.isSubtype(
+                    symbol.enclClass().type, calledMethod.getReturnType(), state);
+          }
         }
-        return super.visitReturn(returnTree, null);
+        return false;
       }
 
       @Override
@@ -156,39 +207,11 @@ public Void visitNewClass(NewClassTree node, Void unused) {
         // don't descend into declarations of anonymous classes
         return null;
       }
-    }.scan(state.getPath(), null);
-
-    if (atLeastOneReturn.get() && allReturnThis.get()) {
-      SuggestedFix.Builder fix = SuggestedFix.builder();
-      String cirvName = qualifyType(state, fix, CIRV);
-
-      // we could add a trailing comment (e.g., @CanIgnoreReturnValue // returns `this`), but all
-      // developers will become familiar with these annotations sooner or later
-      fix.prefixWith(methodTree, "@" + cirvName + "\n");
-
-      return describeMatch(methodTree, fix.build());
-    }
-    return Description.NO_MATCH;
-  }
-
-  /** Returns whether or not the given {@link ReturnTree} returns exactly {@code this}. */
-  private static boolean returnsThis(ReturnTree returnTree, VisitorState state) {
-    ExpressionTree returnExpression = returnTree.getExpression();
-    if (returnExpression instanceof IdentifierTree) {
-      if (((IdentifierTree) returnExpression).getName().contentEquals("this")) {
-        return true;
-      }
-    }
-    if (returnExpression instanceof MethodInvocationTree) {
-      MethodInvocationTree mit = (MethodInvocationTree) returnExpression;
-      if (state.getSourceForNode(mit.getMethodSelect()).contentEquals("self")) {
-        return true;
-      }
     }
-    return false;
-  }
 
-  private static boolean isProtoBuilderSubtype(Type ownerType, VisitorState state) {
-    return isSubtype(ownerType, PROTO_BUILDER.get(state), state);
+    ReturnValuesFromMethodAreIgnorable scanner =
+        new ReturnValuesFromMethodAreIgnorable(state, ASTHelpers.getSymbol(tree));
+    scanner.scan(tree, null);
+    return scanner.atLeastOneReturn && scanner.allReturnsIgnorable;
   }
 }
diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java
index e01941c12f5..ae23907c58a 100644
--- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java
+++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java
@@ -407,4 +407,48 @@ public void testOnlyReturnsSelf_b236423646() {
         .expectUnchanged()
         .doTest();
   }
+
+  @Test
+  public void testDelegateToCirvMethod() {
+    helper
+        .addInputLines(
+            "Client.java",
+            "package com.google.frobber;",
+            "import com.google.errorprone.annotations.CanIgnoreReturnValue;",
+            "import java.util.Arrays;",
+            "import java.util.List;",
+            "public final class Client {",
+            "  public Client setFoo(String... args) {",
+            "    return setFoo(Arrays.asList(args));",
+            "  }",
+            "  public Client setFoos(String... args) {",
+            "    return this.setFoo(Arrays.asList(args));",
+            "  }",
+            "  @CanIgnoreReturnValue",
+            "  public Client setFoo(List args) {",
+            "    return this;",
+            "  }",
+            "}")
+        .addOutputLines(
+            "Client.java",
+            "package com.google.frobber;",
+            "import com.google.errorprone.annotations.CanIgnoreReturnValue;",
+            "import java.util.Arrays;",
+            "import java.util.List;",
+            "public final class Client {",
+            "  @CanIgnoreReturnValue",
+            "  public Client setFoo(String... args) {",
+            "    return setFoo(Arrays.asList(args));",
+            "  }",
+            "  @CanIgnoreReturnValue",
+            "  public Client setFoos(String... args) {",
+            "    return this.setFoo(Arrays.asList(args));",
+            "  }",
+            "  @CanIgnoreReturnValue",
+            "  public Client setFoo(List args) {",
+            "    return this;",
+            "  }",
+            "}")
+        .doTest();
+  }
 }

From 66ec4fd3ee0a1f6ae97a4d5ad7570ea1c0741c5c Mon Sep 17 00:00:00 2001
From: ghm 
Date: Mon, 4 Jul 2022 05:50:26 -0700
Subject: [PATCH 057/102] Run ReturnMissingNullable over ErrorProne.

PiperOrigin-RevId: 458895205
---
 .../java/com/google/errorprone/util/ASTHelpers.java    |  2 ++
 .../com/google/errorprone/util/ErrorProneScope.java    |  3 ++-
 .../bugpatterns/AbstractJUnit4InitMethodNotRun.java    |  3 ++-
 .../google/errorprone/bugpatterns/AlwaysThrows.java    |  3 ++-
 .../errorprone/bugpatterns/ComplexBooleanConstant.java |  7 ++++---
 .../errorprone/bugpatterns/ConstantOverflow.java       | 10 ++++++++++
 .../errorprone/bugpatterns/ConstantPatternCompile.java |  9 +++++----
 .../errorprone/bugpatterns/ForOverrideChecker.java     |  1 +
 .../errorprone/bugpatterns/InfiniteRecursion.java      |  3 ++-
 .../bugpatterns/IsInstanceIncompatibleType.java        |  3 ++-
 .../com/google/errorprone/bugpatterns/JdkObsolete.java |  1 +
 .../errorprone/bugpatterns/LockNotBeforeTry.java       |  3 ++-
 .../bugpatterns/ProtoFieldNullComparison.java          |  5 +++++
 .../errorprone/bugpatterns/ProtoRedundantSet.java      |  7 ++++---
 .../google/errorprone/bugpatterns/SelfAssignment.java  |  3 ++-
 .../errorprone/bugpatterns/TruthAssertExpected.java    |  1 +
 .../errorprone/bugpatterns/UnnecessaryLambda.java      |  5 +++--
 .../bugpatterns/android/IsLoggableTagLength.java       |  1 +
 .../errorprone/bugpatterns/apidiff/ApiDiffChecker.java |  3 ++-
 .../EnclosedByReverseHeuristic.java                    |  3 ++-
 .../LowInformationNameHeuristic.java                   |  3 ++-
 .../AbstractCollectionIncompatibleTypeMatcher.java     |  1 +
 .../CompatibleWithMisuse.java                          |  3 ++-
 .../IncompatibleArgumentType.java                      |  2 ++
 .../bugpatterns/flogger/FloggerArgumentToString.java   |  1 +
 .../bugpatterns/flogger/FloggerRedundantIsEnabled.java |  3 ++-
 .../formatstring/StrictFormatStringValidation.java     |  2 ++
 .../inject/dagger/AndroidInjectionBeforeSuper.java     |  3 ++-
 .../bugpatterns/threadsafety/DoubleCheckedLocking.java |  1 +
 .../bugpatterns/threadsafety/GuardedByBinder.java      |  4 +++-
 .../bugpatterns/threadsafety/GuardedByChecker.java     |  3 ++-
 .../threadsafety/GuardedBySymbolResolver.java          |  5 +++++
 .../bugpatterns/threadsafety/GuardedByUtils.java       |  3 ++-
 .../bugpatterns/threadsafety/ImmutableAnalysis.java    |  3 ++-
 .../bugpatterns/threadsafety/ImmutableChecker.java     |  5 +++--
 .../bugpatterns/threadsafety/ThreadSafety.java         |  3 +++
 .../errorprone/bugpatterns/time/TimeUnitMismatch.java  |  1 +
 .../java/com/google/errorprone/refaster/Bindings.java  |  1 +
 .../java/com/google/errorprone/refaster/Template.java  |  1 +
 .../java/com/google/errorprone/refaster/UBreak.java    |  1 +
 .../java/com/google/errorprone/refaster/URepeated.java |  1 +
 .../annotation/RequiredAnnotationProcessor.java        |  6 ++++--
 42 files changed, 99 insertions(+), 33 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 7ff84c44ac9..62a3c2a110e 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
@@ -1752,6 +1752,7 @@ public Type visitAnnotation(AnnotationTree tree, Void unused) {
       return null;
     }
 
+    @Nullable
     @Override
     public Type visitCase(CaseTree tree, Void unused) {
       Tree t = parent.getParentPath().getLeaf();
@@ -1856,6 +1857,7 @@ public Type visitReturn(ReturnTree tree, Void unused) {
       throw new AssertionError("return not enclosed by method or lambda");
     }
 
+    @Nullable
     @Override
     public Type visitSynchronized(SynchronizedTree node, Void unused) {
       // The null occurs if you've asked for the type of the parentheses around the expression.
diff --git a/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java b/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java
index 6d2c845c6ae..0a9bc79197e 100644
--- a/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java
+++ b/check_api/src/main/java/com/google/errorprone/util/ErrorProneScope.java
@@ -27,6 +27,7 @@
 import java.lang.reflect.Proxy;
 import java.util.Arrays;
 import java.util.function.Predicate;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A compatibility wrapper around {@code com.sun.tools.javac.util.Filter} */
 public final class ErrorProneScope {
@@ -59,7 +60,7 @@ public boolean anyMatch(Predicate predicate) {
 
   private static final Class FILTER_CLASS = getFilterClass();
 
-  private static Class getFilterClass() {
+  private static @Nullable Class getFilterClass() {
     if (RuntimeVersion.isAtLeast17()) {
       return null;
     }
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java
index cec111f45c8..7d32a5c7628 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractJUnit4InitMethodNotRun.java
@@ -37,6 +37,7 @@
 import java.io.Serializable;
 import java.util.List;
 import javax.lang.model.element.Modifier;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Base class for JUnit4SetUp/TearDown not run. This will take care of the nitty-gritty about
@@ -139,7 +140,7 @@ private static void makeProtectedPublic(
     }
   }
 
-  private Description tryToReplaceAnnotation(
+  private @Nullable Description tryToReplaceAnnotation(
       MethodTree methodTree, VisitorState state, String badAnnotation, String goodAnnotation) {
     String finalName = getUnqualifiedClassName(goodAnnotation);
     if (hasAnnotation(badAnnotation).matches(methodTree, state)) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java b/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java
index f1c30c268c6..28665ea5b38 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/AlwaysThrows.java
@@ -48,6 +48,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.util.UUID;
 import java.util.function.Consumer;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
 @BugPattern(summary = "Detects calls that will fail at runtime", severity = ERROR)
@@ -219,7 +220,7 @@ private Description checkImmutableMapOf(
     return checkForRepeatedKeys(tree, keys);
   }
 
-  private Object getConstantKey(ExpressionTree key, VisitorState state) {
+  private @Nullable Object getConstantKey(ExpressionTree key, VisitorState state) {
     return constantExpressions.constantExpression(key, state).orElse(null);
   }
 
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java b/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java
index 1863163862a..25110d00998 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ComplexBooleanConstant.java
@@ -29,6 +29,7 @@
 import com.sun.source.util.SimpleTreeVisitor;
 import com.sun.tools.javac.tree.JCTree.JCLiteral;
 import java.util.Objects;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * @author Sumit Bhagwani (bhagwani@google.com)
@@ -53,7 +54,7 @@ public Description matchBinary(BinaryTree tree, VisitorState state) {
         .build();
   }
 
-  Boolean booleanValue(BinaryTree tree) {
+  @Nullable Boolean booleanValue(BinaryTree tree) {
     if (tree.getLeftOperand() instanceof JCLiteral && tree.getRightOperand() instanceof JCLiteral) {
       return ASTHelpers.constValue(tree, Boolean.class);
     }
@@ -62,7 +63,7 @@ Boolean booleanValue(BinaryTree tree) {
     SimpleTreeVisitor boolValue =
         new SimpleTreeVisitor() {
           @Override
-          public Boolean visitLiteral(LiteralTree node, Void unused) {
+          public @Nullable Boolean visitLiteral(LiteralTree node, Void unused) {
             if (node.getValue() instanceof Boolean) {
               return (Boolean) node.getValue();
             }
@@ -70,7 +71,7 @@ public Boolean visitLiteral(LiteralTree node, Void unused) {
           }
 
           @Override
-          public Boolean visitUnary(UnaryTree node, Void unused) {
+          public @Nullable Boolean visitUnary(UnaryTree node, Void unused) {
             Boolean r = node.getExpression().accept(this, null);
             if (r == null) {
               return null;
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java
index 64a714d3975..62aaef2d276 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantOverflow.java
@@ -110,6 +110,7 @@ private static Fix longFix(ExpressionTree expr, VisitorState state) {
   private static final SimpleTreeVisitor CONSTANT_VISITOR =
       new SimpleTreeVisitor() {
 
+        @Nullable
         @Override
         public Number visitConditionalExpression(ConditionalExpressionTree node, Void p) {
           Number ifTrue = node.getTrueExpression().accept(this, null);
@@ -126,6 +127,7 @@ public Number visitParenthesized(ParenthesizedTree node, Void p) {
           return node.getExpression().accept(this, null);
         }
 
+        @Nullable
         @Override
         public Number visitUnary(UnaryTree node, Void p) {
           Number value = node.getExpression().accept(this, null);
@@ -139,6 +141,7 @@ public Number visitUnary(UnaryTree node, Void p) {
           }
         }
 
+        @Nullable
         @Override
         public Number visitBinary(BinaryTree node, Void p) {
           Number lhs = node.getLeftOperand().accept(this, null);
@@ -170,6 +173,7 @@ public Number visitBinary(BinaryTree node, Void p) {
           }
         }
 
+        @Nullable
         @Override
         public Number visitTypeCast(TypeCastTree node, Void p) {
           Number value = node.getExpression().accept(this, null);
@@ -199,6 +203,7 @@ public Number visitLiteral(LiteralTree node, Void unused) {
         }
       };
 
+  @Nullable
   private static Long unop(Kind kind, long value) {
     switch (kind) {
       case UNARY_PLUS:
@@ -212,6 +217,7 @@ private static Long unop(Kind kind, long value) {
     }
   }
 
+  @Nullable
   private static Integer unop(Kind kind, int value) {
     switch (kind) {
       case UNARY_PLUS:
@@ -225,6 +231,7 @@ private static Integer unop(Kind kind, int value) {
     }
   }
 
+  @Nullable
   static Long binop(Kind kind, long lhs, long rhs) {
     switch (kind) {
       case MULTIPLY:
@@ -254,6 +261,7 @@ static Long binop(Kind kind, long lhs, long rhs) {
     }
   }
 
+  @Nullable
   static Integer binop(Kind kind, int lhs, int rhs) {
     switch (kind) {
       case MULTIPLY:
@@ -283,6 +291,7 @@ static Integer binop(Kind kind, int lhs, int rhs) {
     }
   }
 
+  @Nullable
   private static Number cast(TypeKind kind, Number value) {
     switch (kind) {
       case SHORT:
@@ -300,6 +309,7 @@ private static Number cast(TypeKind kind, Number value) {
     }
   }
 
+  @Nullable
   private static Number getIntegralConstant(Tree node) {
     Number number = ASTHelpers.constValue(node, Number.class);
     if (number instanceof Integer || number instanceof Long) {
diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java
index 4b3bed540cb..9ad76070276 100644
--- a/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java
+++ b/core/src/main/java/com/google/errorprone/bugpatterns/ConstantPatternCompile.java
@@ -53,6 +53,7 @@
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.NestingKind;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Flags variables initialized with {@link java.util.regex.Pattern#compile(String)} calls that could
@@ -206,7 +207,7 @@ public Void visitMemberSelect(MemberSelectTree tree, Void unused) {
   }
 
   /** Infer a name when upgrading the {@code Pattern} local to a constant. */
-  private static String inferName(VariableTree tree, VisitorState state) {
+  private static @Nullable String inferName(VariableTree tree, VisitorState state) {
     String name;
     if ((name = fromName(tree)) != null) {
       return name;
@@ -221,7 +222,7 @@ private static String inferName(VariableTree tree, VisitorState state) {
   }
 
   /** Use the existing local variable's name, unless it's terrible. */
-  private static String fromName(VariableTree tree) {
+  private static @Nullable String fromName(VariableTree tree) {
     String name = LOWER_CAMEL.to(UPPER_UNDERSCORE, tree.getName().toString());
     if (name.length() > 1 && !name.equals("PATTERN")) {
       return name;
@@ -235,7 +236,7 @@ private static String fromName(VariableTree tree) {
    * 

e.g. use {@code FOO_PATTERN} for {@code Pattern.compile(FOO)} and {@code * Pattern.compile(FOO_REGEX)}. */ - private static String fromInitializer(VariableTree tree) { + private static @Nullable String fromInitializer(VariableTree tree) { ExpressionTree regex = ((MethodInvocationTree) tree.getInitializer()).getArguments().get(0); if (!(regex instanceof IdentifierTree)) { return null; @@ -256,7 +257,7 @@ private static String fromInitializer(VariableTree tree) { * If the pattern is only used once in a call to {@code matcher}, and the argument is a local, use * that local's name. For example, infer {@code FOO_PATTERN} from {@code pattern.matcher(foo)}. */ - private static String fromUse(VariableTree tree, VisitorState state) { + private static @Nullable String fromUse(VariableTree tree, VisitorState state) { VarSymbol sym = getSymbol(tree); ImmutableList.Builder usesBuilder = ImmutableList.builder(); new TreePathScanner() { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java index 241c47fd322..c7853f3093c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java @@ -205,6 +205,7 @@ private static ImmutableList getOverriddenMethods( } /** Get the outermost class/interface/enum of an element, or null if none. */ + @Nullable private static Type getOutermostClass(VisitorState state) { return findLast( stream(state.getPath()) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java b/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java index 5056f001cb1..1fb495ef8ca 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/InfiniteRecursion.java @@ -37,6 +37,7 @@ import com.sun.source.tree.TypeCastTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -74,7 +75,7 @@ public MethodInvocationTree visitTypeCast(TypeCastTree tree, Void unused) { } @Override - protected MethodInvocationTree defaultAction(Tree tree, Void unused) { + protected @Nullable MethodInvocationTree defaultAction(Tree tree, Void unused) { return tree instanceof MethodInvocationTree ? (MethodInvocationTree) tree : null; } }, diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java b/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java index cebd9e8b9ef..8c2d9a92caa 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/IsInstanceIncompatibleType.java @@ -38,6 +38,7 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Type; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author cushon@google.com (Liam Miller-Cushon) @@ -84,7 +85,7 @@ public Description matchMemberReference(MemberReferenceTree tree, VisitorState s : buildMessage(argumentType, receiverType, tree, state); } - private static Type classTypeArgument(ExpressionTree tree) { + private static @Nullable Type classTypeArgument(ExpressionTree tree) { ExpressionTree receiver = getReceiver(tree); if (receiver == null) { return null; 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 23176749e12..5806a744ebd 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/JdkObsolete.java @@ -354,6 +354,7 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) { return Optional.of(fix.build()); } + @Nullable private static TreePath findEnclosingMethod(VisitorState state) { TreePath path = state.getPath(); while (path != null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java b/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java index 03ccbcc379a..d82f87ae2d5 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/LockNotBeforeTry.java @@ -39,6 +39,7 @@ import com.sun.source.tree.TryTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreeScanner; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Suggests that calls to {@code Lock.lock} must be immediately followed by a {@code try-finally} @@ -171,7 +172,7 @@ private static boolean releases(TryTree tryTree, ExpressionTree lockee, VisitorS Boolean released = new TreeScanner() { @Override - public Boolean reduce(Boolean r1, Boolean r2) { + public @Nullable Boolean reduce(Boolean r1, Boolean r2) { return r1 == null ? r2 : (r2 == null ? null : r1 && r2); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java index 2e6c07d122e..e54cacb0bc1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java @@ -181,6 +181,7 @@ public Void visitVariable(VariableTree variable, Void unused) { private Optional getInitializer(ExpressionTree tree) { return Optional.ofNullable( new SimpleTreeVisitor() { + @Nullable @Override public ExpressionTree visitMethodInvocation(MethodInvocationTree node, Void unused) { return PROTO_RECEIVER.matches(node, state) ? node : null; @@ -290,6 +291,7 @@ private interface Fixer { private enum GetterTypes { /** {@code proto.getFoo()} */ SCALAR { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -335,6 +337,7 @@ private String replaceLast(String text, String pattern, String replacement) { }, /** {@code proto.getRepeatedFoo(index)} */ VECTOR_INDEXED { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -365,6 +368,7 @@ private String generateFix( }, /** {@code proto.getRepeatedFooList()} */ VECTOR { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (tree.getKind() != Kind.METHOD_INVOCATION) { @@ -391,6 +395,7 @@ private String generateFix( }, /** {@code proto.getField(f)} or {@code proto.getExtension(outer, extension)}; */ EXTENSION_METHOD { + @Nullable @Override Fixer match(ExpressionTree tree, VisitorState state) { if (EXTENSION_METHODS_WITH_NO_FIX.matches(tree, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java index 43dda75068e..b295425a6c6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoRedundantSet.java @@ -41,6 +41,7 @@ import java.util.Collection; import java.util.Map; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Checks that protocol buffers built with chained builders don't set the same field twice. @@ -167,7 +168,7 @@ interface ProtoField {} enum FieldType { SINGLE { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("set") && tree.getArguments().size() == 1) { return FieldWithValue.of(SingleField.of(name), tree, tree.getArguments().get(0)); } @@ -176,7 +177,7 @@ FieldWithValue match(String name, MethodInvocationTree tree) { }, REPEATED { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("set") && tree.getArguments().size() == 2) { Integer index = ASTHelpers.constValue(tree.getArguments().get(0), Integer.class); if (index != null) { @@ -189,7 +190,7 @@ FieldWithValue match(String name, MethodInvocationTree tree) { }, MAP { @Override - FieldWithValue match(String name, MethodInvocationTree tree) { + @Nullable FieldWithValue match(String name, MethodInvocationTree tree) { if (name.startsWith("put") && tree.getArguments().size() == 2) { Object key = ASTHelpers.constValue(tree.getArguments().get(0), Object.class); if (key != null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java index 019c24214cf..8d0175874b6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SelfAssignment.java @@ -49,6 +49,7 @@ import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; +import org.checkerframework.checker.nullness.qual.Nullable; /** * TODO(eaftan): Consider cases where the parent is not a statement or there is no parent? @@ -122,7 +123,7 @@ public ExpressionTree visitTypeCast(TypeCastTree node, Void unused) { } @Override - protected ExpressionTree defaultAction(Tree node, Void unused) { + protected @Nullable ExpressionTree defaultAction(Tree node, Void unused) { return node instanceof ExpressionTree ? (ExpressionTree) node : null; } }.visit(expression, null); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java b/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java index 4753a66e46a..a974b30ad47 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/TruthAssertExpected.java @@ -132,6 +132,7 @@ public List visitNewClass(NewClassTree node, Void unus return node.getArguments(); } + @Nullable @Override public List visitMethodInvocation( MethodInvocationTree node, Void unused) { 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 bcb19bb6db1..abacf67f7d1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryLambda.java @@ -66,6 +66,7 @@ import java.util.function.Predicate; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -303,7 +304,7 @@ public LambdaExpressionTree visitLambdaExpression(LambdaExpressionTree node, Voi } @Override - public LambdaExpressionTree visitBlock(BlockTree node, Void unused) { + public @Nullable LambdaExpressionTree visitBlock(BlockTree node, Void unused) { // when processing a method body, only consider methods with a single `return` statement // that returns a method return node.getStatements().size() == 1 @@ -312,7 +313,7 @@ public LambdaExpressionTree visitBlock(BlockTree node, Void unused) { } @Override - public LambdaExpressionTree visitReturn(ReturnTree node, Void unused) { + public @Nullable LambdaExpressionTree visitReturn(ReturnTree node, Void unused) { return node.getExpression() != null ? node.getExpression().accept(this, null) : null; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java b/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java index 20740965663..2602353ea37 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/android/IsLoggableTagLength.java @@ -112,6 +112,7 @@ private static VariableTree findEnclosingIdentifier( .findEnclosing(ClassTree.class) .accept( new TreeScanner() { + @Nullable @Override public VariableTree visitVariable(VariableTree node, Void p) { return getSymbol(node).equals(identifierSymbol) ? node : null; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java index aaee0fb2b55..a2d1afcafe9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/apidiff/ApiDiffChecker.java @@ -38,6 +38,7 @@ import com.sun.tools.javac.code.Types; import java.lang.annotation.Annotation; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** A base Error Prone check implementation to enforce compliance with a given API diff. */ public abstract class ApiDiffChecker extends BugChecker @@ -125,7 +126,7 @@ private boolean hasAnnotationForbiddingUse(Symbol sym, VisitorState state) { * Finds the class of the expression's receiver: the declaring class of a static member access, or * the type that an instance member is accessed on. */ - private static ClassSymbol getReceiver(ExpressionTree tree, Symbol sym) { + private static @Nullable ClassSymbol getReceiver(ExpressionTree tree, Symbol sym) { if (sym.isStatic() || sym instanceof ClassSymbol) { return sym.enclClass(); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java index 2ade769023c..54935fa59b6 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/EnclosedByReverseHeuristic.java @@ -24,6 +24,7 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol.MethodSymbol; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Detect whether the method invocation we are examining is enclosed by either a method or a class @@ -74,7 +75,7 @@ public boolean isAcceptableChange( return findReverseWordsMatchInParentNodes(state) == null; } - protected String findReverseWordsMatchInParentNodes(VisitorState state) { + protected @Nullable String findReverseWordsMatchInParentNodes(VisitorState state) { for (Tree tree : state.getPath()) { Optional name = getName(tree); if (name.isPresent()) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java index 2e0a6df3601..1aa2adaedef 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/LowInformationNameHeuristic.java @@ -20,6 +20,7 @@ import com.google.errorprone.VisitorState; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A heuristic for checking if a formal parameter matches a predefined set of words which have been @@ -58,7 +59,7 @@ public boolean isAcceptableChange( * Return the first regular expression from the list of overloaded words which matches the * parameter name. */ - protected String findMatch(Parameter parameter) { + protected @Nullable String findMatch(Parameter parameter) { for (String regex : overloadedNamesRegexs) { if (parameter.name().matches(regex)) { return regex; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java index 995e9a42cdf..59184c9956f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/AbstractCollectionIncompatibleTypeMatcher.java @@ -160,6 +160,7 @@ public MatchResult visitMemberReference( }.visit(tree, null); } + @Nullable private MatchResult getMatchResult( @Nullable ExpressionTree sourceTree, @Nullable Type sourceType, @Nullable Type targetType) { if (sourceTree == null || sourceType == null || targetType == null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java index d087ac9644b..e81e5eedd47 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/CompatibleWithMisuse.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author glorioso@google.com (Nick Glorioso) @@ -127,7 +128,7 @@ public Description matchAnnotation(AnnotationTree annoTree, VisitorState state) // => X // This function assumes the annotation tree will only have one argument, of type String, that // is required. - private static String valueArgumentFromCompatibleWithAnnotation(AnnotationTree tree) { + private static @Nullable String valueArgumentFromCompatibleWithAnnotation(AnnotationTree tree) { ExpressionTree argumentValue = Iterables.getOnlyElement(tree.getArguments()); if (argumentValue.getKind() != Kind.ASSIGNMENT) { // :-| Annotation symbol broken. Punt? diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java index b2e5c72032a..d1e864b5263 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/collectionincompatibletype/IncompatibleArgumentType.java @@ -225,6 +225,7 @@ private static RequiredType resolveRequiredTypeForThisCall( return requiredType; } + @Nullable private static RequiredType resolveTypeFromGenericMethod( Type calledMethodType, MethodSymbol declaredMethod, String typeArgName) { int tyargIndex = findTypeArgInList(declaredMethod, typeArgName); @@ -236,6 +237,7 @@ private static RequiredType resolveTypeFromGenericMethod( // Plumb through a type which is supposed to be a Types.Subst, then find the replacement // type that the compiler resolved. + @Nullable private static Type getTypeFromTypeMapping( Type m, MethodSymbol declaredMethod, String namedTypeArg) { ImmutableListMultimap substitutions = diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java index b7a9b762c2c..08e0a325c0e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerArgumentToString.java @@ -355,6 +355,7 @@ Description unwrapArguments( return describeMatch(tree, fix.build()); } + @Nullable private static Parameter unwrap(ExpressionTree argument, char placeholder, VisitorState state) { for (Unwrapper unwrapper : Unwrapper.values()) { if (unwrapper.matcher.matches(argument, state)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java index e41106dc8bb..6d42b3f0fbe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/flogger/FloggerRedundantIsEnabled.java @@ -47,6 +47,7 @@ import com.sun.tools.javac.code.Symbol; import java.util.List; import java.util.Optional; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author mariasam@google.com (Maria Sam) @@ -136,7 +137,7 @@ private static ExpressionTree unwrap(ExpressionTree expr) { new SimpleTreeVisitor() { @Override - protected ExpressionTree defaultAction(Tree tree, Void unused) { + protected @Nullable ExpressionTree defaultAction(Tree tree, Void unused) { return tree instanceof ExpressionTree ? (ExpressionTree) tree : null; } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java index 2d5ec6ab1df..43c047cb73e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/formatstring/StrictFormatStringValidation.java @@ -100,6 +100,7 @@ public static ValidationResult validate( } /** Helps {@code validate()} validate a format string that is declared as a method parameter. */ + @Nullable private static ValidationResult validateFormatStringParameter( ExpressionTree formatStringTree, Symbol formatStringSymbol, @@ -219,6 +220,7 @@ public ValidationResult visitVariable(VariableTree node, Void unused) { return super.visitVariable(node, unused); } + @Nullable @Override public ValidationResult reduce(ValidationResult r1, ValidationResult r2) { if (r1 == null && r2 == null) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java index f7168ff2738..a378f57b23c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/inject/dagger/AndroidInjectionBeforeSuper.java @@ -42,6 +42,7 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.SimpleTreeVisitor; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author Ron Shapiro @@ -127,7 +128,7 @@ private final class LifecycleMethodVisitor extends SimpleTreeVisitor argumen return new AutoValue_GuardedBySymbolResolver_MethodInfo(sym, arguments); } + @Nullable static MethodInfo create(Tree tree, VisitorState visitorState) { Symbol sym = ASTHelpers.getSymbol(tree); if (!(sym instanceof MethodSymbol)) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java index b50ecb72ee6..6040e296098 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/GuardedByUtils.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; /** * @author cushon@google.com (Liam Miller-Cushon) @@ -137,7 +138,7 @@ public static GuardedByValidationResult isGuardedByValid( return GuardedByValidationResult.ok(); } - public static Symbol bindGuardedByString( + public static @Nullable Symbol bindGuardedByString( Tree tree, String guard, VisitorState visitorState, GuardedByFlags flags) { Optional bound = GuardedByBinder.bindString(guard, GuardedBySymbolResolver.from(tree, visitorState), flags); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java index 62c6107a9ce..496e2663b46 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java @@ -47,6 +47,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.nullness.qual.Nullable; /** Analyzes types for deep immutability. */ public class ImmutableAnalysis { @@ -323,7 +324,7 @@ AnnotationInfo getImmutableAnnotation(Symbol sym, VisitorState state) { * Gets the {@link Tree}'s {@code @Immutable} annotation info, either from an annotation on the * symbol or from the list of well-known immutable types. */ - AnnotationInfo getImmutableAnnotation(Tree tree, VisitorState state) { + @Nullable AnnotationInfo getImmutableAnnotation(Tree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); return sym == null ? null : threadSafety.getMarkerOrAcceptedAnnotation(sym, state); } 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 6fbb5d0ebfb..df41d97c401 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 @@ -82,6 +82,7 @@ import java.util.Optional; import java.util.Set; import javax.lang.model.element.ElementKind; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ @BugPattern( @@ -544,7 +545,7 @@ private Description.Builder describeAnonymous(Tree tree, Type superType, Violati * Check for classes with {@code @Immutable}, or that inherited it from a super class or * interface. */ - private AnnotationInfo getImmutableAnnotation( + private @Nullable AnnotationInfo getImmutableAnnotation( ImmutableAnalysis analysis, ClassTree tree, VisitorState state) { AnnotationInfo annotation = analysis.getImmutableAnnotation(tree, state); if (annotation != null) { @@ -563,7 +564,7 @@ private AnnotationInfo getImmutableAnnotation( * Returns the type of the first superclass or superinterface in the hierarchy annotated with * {@code @Immutable}, or {@code null} if no such super type exists. */ - private Type immutableSupertype(Symbol sym, VisitorState state) { + private @Nullable Type immutableSupertype(Symbol sym, VisitorState state) { for (Type superType : state.getTypes().closure(sym.type)) { if (superType.tsym.equals(sym.type.tsym)) { continue; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java index 2c80a2d9b40..8665c5ac6bf 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java @@ -610,6 +610,7 @@ public AnnotationInfo getMarkerOrAcceptedAnnotation(Symbol sym, VisitorState sta } /** Returns an enclosing instance for the specified type if it is thread-safe. */ + @Nullable public Type mutableEnclosingInstance(Optional tree, ClassType type) { if (tree.isPresent() && !CanBeStaticAnalyzer.referencesOuter( @@ -675,6 +676,7 @@ public Set threadSafeTypeParametersInScope(Symbol sym) { return result.build(); } + @Nullable private AnnotationInfo getAnnotation( Symbol sym, ImmutableSet annotationsToCheck, VisitorState state) { for (String annotation : annotationsToCheck) { @@ -686,6 +688,7 @@ private AnnotationInfo getAnnotation( return null; } + @Nullable private AnnotationInfo getAnnotation( Symbol sym, VisitorState state, diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java index 0d291d8bb7b..38329e38085 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/TimeUnitMismatch.java @@ -371,6 +371,7 @@ private static String extractArgumentName(ExpressionTree expr) { isSameType("java.lang.Long"), isSameType("java.lang.Double")); + @Nullable @VisibleForTesting static TimeUnit unitSuggestedByName(String name) { // Tuple types, especially Pair, trip us up. Skip APIs that might be from them. diff --git a/core/src/main/java/com/google/errorprone/refaster/Bindings.java b/core/src/main/java/com/google/errorprone/refaster/Bindings.java index 6904e63ee69..b96fdaa0528 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Bindings.java +++ b/core/src/main/java/com/google/errorprone/refaster/Bindings.java @@ -128,6 +128,7 @@ public V putBinding(Key key, V value) { return (V) super.put(key, value); } + @Nullable @Override public Object put(Key key, Object value) { checkNotNull(key, "key"); 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 0fbbc435cf5..7c1a2712e65 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Template.java +++ b/core/src/main/java/com/google/errorprone/refaster/Template.java @@ -492,6 +492,7 @@ private Type infer( } /** Reflectively instantiate the package-private {@code MethodResolutionPhase} enum. */ + @Nullable private static Object newMethodResolutionPhase(boolean autoboxing) { for (Class c : Resolve.class.getDeclaredClasses()) { if (!c.getName().equals("com.sun.tools.javac.comp.Resolve$MethodResolutionPhase")) { diff --git a/core/src/main/java/com/google/errorprone/refaster/UBreak.java b/core/src/main/java/com/google/errorprone/refaster/UBreak.java index b552ac08aba..5aadd598140 100644 --- a/core/src/main/java/com/google/errorprone/refaster/UBreak.java +++ b/core/src/main/java/com/google/errorprone/refaster/UBreak.java @@ -45,6 +45,7 @@ static UBreak create(@Nullable CharSequence label) { } // TODO(b/176098078): Add @Override once compiling JDK 12+ + @Nullable public ExpressionTree getValue() { return null; } diff --git a/core/src/main/java/com/google/errorprone/refaster/URepeated.java b/core/src/main/java/com/google/errorprone/refaster/URepeated.java index a537d735666..36059637d31 100644 --- a/core/src/main/java/com/google/errorprone/refaster/URepeated.java +++ b/core/src/main/java/com/google/errorprone/refaster/URepeated.java @@ -57,6 +57,7 @@ public Kind getKind() { } /** Gets the binding of the underlying identifier in the unifier. */ + @Nullable public JCExpression getUnderlyingBinding(Unifier unifier) { return (unifier == null) ? null : unifier.getBinding(new UFreeIdent.Key(identifier())); } diff --git a/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java b/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java index 0c7b41bd5f8..012a2960916 100644 --- a/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java +++ b/core/src/main/java/com/google/errorprone/refaster/annotation/RequiredAnnotationProcessor.java @@ -34,6 +34,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleAnnotationValueVisitor7; import javax.tools.Diagnostic.Kind; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Enforces {@code @RequiredAnnotation} as an annotation processor. @@ -51,7 +52,8 @@ public boolean process(Set annotations, RoundEnvironment return false; } - private AnnotationMirror getAnnotationMirror(Element element, TypeMirror annotationType) { + private @Nullable AnnotationMirror getAnnotationMirror( + Element element, TypeMirror annotationType) { for (AnnotationMirror mirror : element.getAnnotationMirrors()) { if (processingEnv.getTypeUtils().isSameType(mirror.getAnnotationType(), annotationType)) { return mirror; @@ -60,7 +62,7 @@ private AnnotationMirror getAnnotationMirror(Element element, TypeMirror annotat return null; } - private AnnotationValue getAnnotationValue(AnnotationMirror mirror, String key) { + private @Nullable AnnotationValue getAnnotationValue(AnnotationMirror mirror, String key) { for (Map.Entry entry : mirror.getElementValues().entrySet()) { if (entry.getKey().getSimpleName().contentEquals(key)) { From 183c709f73137329f5bcfeb08364784252b4d2a6 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 5 Jul 2022 09:42:46 -0700 Subject: [PATCH 058/102] Explain the motivation of BooleanParameter a little. PiperOrigin-RevId: 459092850 --- docs/bugpattern/BooleanParameter.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/bugpattern/BooleanParameter.md b/docs/bugpattern/BooleanParameter.md index e69de29bb2d..b75477c2843 100644 --- a/docs/bugpattern/BooleanParameter.md +++ b/docs/bugpattern/BooleanParameter.md @@ -0,0 +1,7 @@ +Providing parameter comments for boolean literals has some advantages: + +* Readability is generally improved, as the parameter name will likely provide + some context on what the boolean literal means +* [https://errorprone.info/bugpattern/ParameterName](ParameterName) checks at compile-time that the + comments match the formal argument names to avoid accidentally transposing + parameters From b205bae3e8d7315d106a5e9d48dea01c4c57b52b Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 6 Jul 2022 10:17:54 -0700 Subject: [PATCH 059/102] Remove some more constant conditions. PiperOrigin-RevId: 459282605 --- .../main/java/com/google/errorprone/util/ASTHelpers.java | 5 +---- .../java/com/google/errorprone/util/FindIdentifiers.java | 9 +-------- .../java/com/google/errorprone/util/Reachability.java | 3 --- .../bugpatterns/nullness/VoidMissingNullable.java | 3 --- 4 files changed, 2 insertions(+), 18 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 62a3c2a110e..d56da359ac4 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 @@ -2226,10 +2226,7 @@ public Void visitThrow(ThrowTree tree, Void unused) { @Override public Void visitNewClass(NewClassTree tree, Void unused) { - MethodSymbol symbol = getSymbol(tree); - if (symbol != null) { - getThrownTypes().addAll(symbol.getThrownTypes()); - } + getThrownTypes().addAll(getSymbol(tree).getThrownTypes()); return super.visitNewClass(tree, null); } diff --git a/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java b/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java index d644a5fbbb8..76dfca39cc5 100644 --- a/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java +++ b/check_api/src/main/java/com/google/errorprone/util/FindIdentifiers.java @@ -132,7 +132,7 @@ private static ClassTree getEnclosingClass(TreePath treePath) { return (ClassTree) treePath.getLeaf(); } - while (treePath != null) { + while (true) { TreePath parent = treePath.getParentPath(); if (parent == null) { return null; @@ -144,7 +144,6 @@ private static ClassTree getEnclosingClass(TreePath treePath) { } treePath = parent; } - return null; } /** @@ -486,12 +485,6 @@ private static boolean inStaticContext(TreePath path) { break; case METHOD_INVOCATION: // JLS 8.8.7.1 explicit constructor invocation MethodSymbol methodSym = ASTHelpers.getSymbol((MethodInvocationTree) tree); - if (methodSym == null) { - // sometimes javac can't resolve the symbol. In this case just assume that we are - // in a static context - this is a safe approximation in our context (checking - // visibility) - return true; - } if (methodSym.isConstructor() && (Objects.equals(methodSym.owner, enclosingClass) || Objects.equals(methodSym.owner, directSuperClass))) { diff --git a/check_api/src/main/java/com/google/errorprone/util/Reachability.java b/check_api/src/main/java/com/google/errorprone/util/Reachability.java index 1d34f65f658..67d31bf7d9c 100644 --- a/check_api/src/main/java/com/google/errorprone/util/Reachability.java +++ b/check_api/src/main/java/com/google/errorprone/util/Reachability.java @@ -167,9 +167,6 @@ private static boolean isSystemExit(ExpressionTree expression) { return false; } MethodSymbol sym = getSymbol((MethodInvocationTree) expression); - if (sym == null) { - return false; - } return sym.owner.getQualifiedName().contentEquals("java.lang.System") && sym.getSimpleName().contentEquals("exit"); } 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 629746f5de2..132a7f15514 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 @@ -110,9 +110,6 @@ 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); } } From a0a258a5efbc01a70c26d534acfbe8be406d296b Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 7 Jul 2022 04:16:00 -0700 Subject: [PATCH 060/102] Only emit one finding for contiguous sequences of Unicode characters. PiperOrigin-RevId: 459476090 --- .../errorprone/bugpatterns/UnicodeInCode.java | 23 ++++++++++--------- .../bugpatterns/UnicodeInCodeTest.java | 12 ++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java index 98ee491645d..e5c72add1d4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnicodeInCode.java @@ -21,22 +21,22 @@ import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ErrorProneTokens.getTokens; +import static com.google.errorprone.util.SourceCodeEscapers.javaCharEscaper; import static java.lang.String.format; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; import com.google.errorprone.fixes.FixedPosition; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ErrorProneToken; -import com.google.errorprone.util.SourceCodeEscapers; import com.sun.source.tree.CompilationUnitTree; import com.sun.tools.javac.parser.Tokens.TokenKind; -import java.util.LinkedHashMap; -import java.util.Map; /** Bans using non-ASCII Unicode characters outside string literals and comments. */ @BugPattern( @@ -47,14 +47,14 @@ public final class UnicodeInCode extends BugChecker implements CompilationUnitTreeMatcher { @Override public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { - Map violations = new LinkedHashMap<>(); + RangeSet violations = TreeRangeSet.create(); String sourceCode = state.getSourceCode().toString(); for (int i = 0; i < sourceCode.length(); ++i) { char c = sourceCode.charAt(i); if (!isAcceptableAscii(c)) { - violations.put(i, c); + violations.add(Range.closedOpen(i, i + 1)); } } @@ -65,17 +65,18 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s ImmutableRangeSet permissibleUnicodeRegions = suppressedRegions(state).union(commentsAndLiterals(state, sourceCode)); - for (var e : violations.entrySet()) { - int violatingLocation = e.getKey(); - char c = e.getValue(); - if (!permissibleUnicodeRegions.contains(violatingLocation)) { + for (var range : violations.asDescendingSetOfRanges()) { + if (!permissibleUnicodeRegions.encloses(range)) { state.reportMatch( - buildDescription(new FixedPosition(tree, violatingLocation)) + buildDescription(new FixedPosition(tree, range.lowerEndpoint())) .setMessage( format( "Avoid using non-ASCII Unicode character (%s) outside of comments and" + " literals, as they can be confusing.", - SourceCodeEscapers.javaCharEscaper().escape(Character.toString(c)))) + javaCharEscaper() + .escape( + sourceCode.substring( + range.lowerEndpoint(), range.upperEndpoint())))) .build()); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java index 36b10a5147d..31af8151454 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnicodeInCodeTest.java @@ -84,6 +84,18 @@ public void positive() { .doTest(); } + @Test + public void positiveMultiCharacterGivesOneFinding() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " // BUG: Diagnostic contains: Unicode character (\\u03c0\\u03c0)", + " static final double \u03C0\u03C0 = 3;", + "}") + .doTest(); + } + @Test public void suppressibleAtClassLevel() { helper From 080011d701e91c3533c5cba446821e701c79cdac Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 7 Jul 2022 09:23:32 -0700 Subject: [PATCH 061/102] Complain about totally unused type parameters. I'm sure there's more we could do here (presumably a type param which only appears in the argument list could be replaced with Object, for example), but this is a start. PiperOrigin-RevId: 459530286 --- .../errorprone/fixes/SuggestedFixes.java | 2 +- .../bugpatterns/UnusedTypeParameter.java | 132 ++++++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../bugpatterns/UnusedTypeParameterTest.java | 118 ++++++++++++++++ 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java index d2cd507ce26..3fb801e4790 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java @@ -567,7 +567,7 @@ public static void qualifyDocReference( * parentheses if no elements are left. */ public static SuggestedFix removeElement( - ExpressionTree tree, List trees, VisitorState state) { + Tree tree, List trees, VisitorState state) { int indexOf = trees.indexOf(tree); checkArgument(indexOf != -1, "trees must contain tree"); if (trees.size() == 1) { diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.java new file mode 100644 index 00000000000..32221134d03 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnusedTypeParameter.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; + +import static com.google.common.collect.Iterables.getLast; +import static com.google.errorprone.fixes.SuggestedFixes.removeElement; +import static com.google.errorprone.util.ASTHelpers.findSuperMethods; +import static com.google.errorprone.util.ASTHelpers.getStartPosition; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.methodCanBeOverridden; + +import com.google.common.collect.ImmutableMultiset; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ErrorProneTokens; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeParameterTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import java.util.List; + +/** A BugPattern; see the summary. */ +@BugPattern( + severity = SeverityLevel.WARNING, + summary = "This type parameter is unused and can be removed.") +public final class UnusedTypeParameter extends BugChecker implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + var usedIdentifiers = findUsedIdentifiers(tree); + new SuppressibleTreePathScanner(state) { + @Override + public Void visitClass(ClassTree node, Void unused) { + if ((getSymbol(node).flags() & Flags.FINAL) != 0) { + handle(node, node.getTypeParameters()); + } + return super.visitClass(node, null); + } + + @Override + public Void visitMethod(MethodTree node, Void unused) { + var symbol = getSymbol(node); + if (methodCanBeOverridden(symbol) + || !findSuperMethods(symbol, state.getTypes()).isEmpty()) { + return null; + } + handle(node, node.getTypeParameters()); + return super.visitMethod(node, null); + } + + private void handle(Tree tree, List typeParameters) { + for (TypeParameterTree typeParameter : typeParameters) { + if (usedIdentifiers.count(getSymbol(typeParameter)) == 1) { + state.reportMatch( + describeMatch( + typeParameter, + removeTypeParameter(tree, typeParameter, typeParameters, state))); + } + } + } + }.scan(state.getPath(), null); + return Description.NO_MATCH; + } + + private static ImmutableMultiset findUsedIdentifiers( + CompilationUnitTree tree) { + ImmutableMultiset.Builder identifiers = ImmutableMultiset.builder(); + new TreeScanner() { + @Override + public Void scan(Tree tree, Void unused) { + var symbol = getSymbol(tree); + if (symbol instanceof TypeVariableSymbol) { + identifiers.add((TypeVariableSymbol) symbol); + } + return super.scan(tree, unused); + } + }.scan(tree, null); + return identifiers.build(); + } + + private static SuggestedFix removeTypeParameter( + Tree tree, + TypeParameterTree typeParameter, + List typeParameters, + VisitorState state) { + if (typeParameters.size() > 1) { + return removeElement(typeParameter, typeParameters, state); + } + var tokens = + ErrorProneTokens.getTokens( + state.getSourceForNode(tree), getStartPosition(tree), state.context); + int startPos = + tokens.reverse().stream() + .filter( + t -> t.pos() <= getStartPosition(typeParameter) && t.kind().equals(TokenKind.LT)) + .findFirst() + .get() + .pos(); + int endPos = + tokens.stream() + .filter( + t -> + t.endPos() >= state.getEndPosition(getLast(typeParameters)) + && (t.kind().equals(TokenKind.GT) || t.kind().equals(TokenKind.GTGT))) + .findFirst() + .get() + .endPos(); + return SuggestedFix.replace(startPos, endPos, ""); + } +} 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 9bdb46394f8..2cb9e525522 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -388,6 +388,7 @@ import com.google.errorprone.bugpatterns.UnusedException; import com.google.errorprone.bugpatterns.UnusedMethod; import com.google.errorprone.bugpatterns.UnusedNestedClass; +import com.google.errorprone.bugpatterns.UnusedTypeParameter; import com.google.errorprone.bugpatterns.UnusedVariable; import com.google.errorprone.bugpatterns.UseCorrectAssertInTests; import com.google.errorprone.bugpatterns.UseEnumSwitch; @@ -997,6 +998,7 @@ public static ScannerSupplier errorChecks() { UnsynchronizedOverridesSynchronized.class, UnusedMethod.class, UnusedNestedClass.class, + UnusedTypeParameter.class, UnusedVariable.class, UseBinds.class, UseCorrectAssertInTests.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java new file mode 100644 index 00000000000..f1bd51972e8 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnusedTypeParameterTest.java @@ -0,0 +1,118 @@ +/* + * 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.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class UnusedTypeParameterTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(UnusedTypeParameter.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(UnusedTypeParameter.class, getClass()); + + @Test + public void positiveOnClass() { + helper + .addSourceLines( + "Test.java", // + "// BUG: Diagnostic contains:", + "final class Test {}") + .doTest(); + } + + @Test + public void refactoring() { + refactoring + .addInputLines( + "Test.java", // + "final class Test {}") + .addOutputLines( + "Test.java", // + "final class Test {}") + .doTest(); + } + + @Test + public void refactoringWithTwoParameters() { + refactoring + .addInputLines( + "Test.java", // + "final class Test {", + " B get() { return null; }", + "}") + .addOutputLines( + "Test.java", // + "final class Test {", + " B get() { return null; }", + "}") + .doTest(); + } + + @Test + public void refactoringWithGtgt() { + refactoring + .addInputLines( + "Test.java", // + "final class Test> {}") + .addOutputLines( + "Test.java", // + "final class Test {}") + .doTest(); + } + + @Test + public void positiveOnMethod() { + helper + .addSourceLines( + "Test.java", // + "final class Test {", + " // BUG: Diagnostic contains:", + " private void test() {}", + "}") + .doTest(); + } + + @Test + public void methodCouldBeOverridden_negativeFinding() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " void test() {}", + "}") + .doTest(); + } + + @Test + public void negative() { + helper + .addSourceLines( + "Test.java", // + "class Test {", + " private boolean contains(java.util.Set set, T elem) {", + " return set.contains(elem);", + " }", + "}") + .doTest(); + } +} From 2f5e8f1bf650d881f8d96c315080332bcfa94c5b Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 8 Jul 2022 05:36:04 -0700 Subject: [PATCH 062/102] Have #addSuppressWarnings throw if there's no node to which to attach a @SW, rather than returning a surprise null. I've never written code which would handle the `null` case when using this method, and somehow I suspect no one has... PiperOrigin-RevId: 459737317 --- .../java/com/google/errorprone/fixes/SuggestedFixes.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java index 3fb801e4790..e5655402cda 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFixes.java @@ -926,7 +926,6 @@ private static int getThrowsPosition(MethodTree tree, VisitorState state) { * * @see #addSuppressWarnings(VisitorState, String, String) */ - @Nullable public static SuggestedFix addSuppressWarnings(VisitorState state, String warningToSuppress) { return addSuppressWarnings(state, warningToSuppress, null); } @@ -942,14 +941,16 @@ public static SuggestedFix addSuppressWarnings(VisitorState state, String warnin *

In the event that a suppressible element couldn't be found (e.g.: the state is pointing at a * CompilationUnit, or some other internal inconsistency has occurred), or the enclosing * suppressible element already has a {@code @SuppressWarnings} annotation with {@code - * warningToSuppress}, this method will return null. + * warningToSuppress}, this method will throw an {@link IllegalArgumentException}. */ - @Nullable public static SuggestedFix addSuppressWarnings( VisitorState state, String warningToSuppress, @Nullable String lineComment) { SuggestedFix.Builder fixBuilder = SuggestedFix.builder(); addSuppressWarnings(fixBuilder, state, warningToSuppress, lineComment); - return fixBuilder.isEmpty() ? null : fixBuilder.build(); + if (fixBuilder.isEmpty()) { + throw new IllegalArgumentException("Couldn't find a node to attach @SuppressWarnings."); + } + return fixBuilder.build(); } /** From f8e26c68c3adaa25038bdf5e389863e1423caeaf Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Fri, 8 Jul 2022 14:04:16 -0700 Subject: [PATCH 063/102] Allow CheckReturnValue to detect other forms of mutating accessors for protobuf libraries. PiperOrigin-RevId: 459830183 --- .../checkreturnvalue/ProtoRules.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 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 ae3c334d82b..8350f68c5a1 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 @@ -53,8 +53,10 @@ public static ResultUseRule mutableProtos() { /** 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).*"); + // Methods that start this way produce a modification to the proto, and either return this + // or return the parameter given, for chaining purposes. + private static final Pattern NAMED_MUTATOR_METHOD = + Pattern.compile("(add|clear|insert|merge|remove|set|put).*"); private final Supplier parentType; private final String id; @@ -73,11 +75,10 @@ public String id() { public Optional evaluateMethod(MethodSymbol method, VisitorState state) { if (isProtoSubtype(method.owner.type, state)) { String methodName = method.name.toString(); - if (RETURNS_THIS.matcher(methodName).matches()) { + if (NAMED_MUTATOR_METHOD.matcher(methodName).matches()) { return Optional.of(ResultUsePolicy.OPTIONAL); } - if (isGetterOfSubmessageBuilder(methodName) - && isProtoSubtype(method.getReturnType(), state)) { + if (isMutatingAccessorMethod(methodName) && isProtoSubtype(method.getReturnType(), state)) { return Optional.of(ResultUsePolicy.OPTIONAL); } } @@ -88,11 +89,16 @@ 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) { + private static boolean isMutatingAccessorMethod(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"); + if (name.startsWith("get")) { + // fooBuilder.getBarBuilder() mutates the builder such that foo.hasBar() is now true. + return (name.endsWith("Builder") && !name.endsWith("OrBuilder")) + // mutableFoo.getMutableBar() mutates Foo so that mutableFoo.hasBar() is now true + || name.startsWith("getMutable"); + } + return false; } } From b3c4fa8963c05fad7680f868fca1557b71914d22 Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 11 Jul 2022 04:48:10 -0700 Subject: [PATCH 064/102] MockNotUsedInProduction: complain about mocks which aren't used except for when/verify calls. PiperOrigin-RevId: 460181545 --- .../bugpatterns/MockNotUsedInProduction.java | 199 +++++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../MockNotUsedInProductionTest.java | 232 ++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java new file mode 100644 index 00000000000..4baa65b6ce3 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java @@ -0,0 +1,199 @@ +/* + * 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.Streams.stream; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.anyMethod; +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.hasAnnotation; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.util.ASTHelpers.canBeRemoved; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static java.util.stream.Stream.concat; +import static javax.lang.model.element.ElementKind.FIELD; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +/** A BugPattern; see the summary. */ +@BugPattern( + severity = WARNING, + summary = + "This mock is configured but never escapes to be used in production code. Should it be" + + " removed?") +public final class MockNotUsedInProduction extends BugChecker + implements CompilationUnitTreeMatcher { + @Override + public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { + ImmutableMap mocks = findMocks(state); + if (mocks.isEmpty()) { + return NO_MATCH; + } + Set usedMocks = new HashSet<>(); + new TreePathScanner() { + @Override + public Void visitMethodInvocation(MethodInvocationTree invocation, Void unused) { + // Don't count references to mocks within the arguments of a when(...) call to be a usage. + // We still need to scan the receiver for the case of + // `doReturn(someMockWhichIsAUsage).when(aMockWhichIsNotAUsage);` + if (WHEN_OR_VERIFY.matches(invocation, state)) { + scan(invocation.getMethodSelect(), null); + return null; + } + return super.visitMethodInvocation(invocation, null); + } + + @Override + public Void visitMemberSelect(MemberSelectTree memberSelect, Void unused) { + handle(memberSelect); + return super.visitMemberSelect(memberSelect, null); + } + + @Override + public Void visitIdentifier(IdentifierTree identifier, Void unused) { + handle(identifier); + return super.visitIdentifier(identifier, null); + } + + private void handle(Tree tree) { + var symbol = getSymbol(tree); + if (symbol instanceof VarSymbol) { + usedMocks.add((VarSymbol) symbol); + } + } + }.scan(state.getPath(), null); + mocks.forEach( + (sym, mockTree) -> { + if (usedMocks.contains(sym)) { + return; + } + state.reportMatch(describeMatch(mockTree, generateFix(sym, state))); + }); + return NO_MATCH; + } + + /** + * Very crudely deletes every variable or expression statement which contains a reference to + * {@code sym}. This is inefficient insofar as we scan the entire file again, but only when + * generating a fix. + */ + private static SuggestedFix generateFix(VarSymbol sym, VisitorState state) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + new TreePathScanner() { + + @Override + public Void scan(Tree tree, Void unused) { + if (Objects.equals(getSymbol(tree), sym)) { + // Yes, at this point, the current path hasn't been updated to include `tree`... + concat(Stream.of(tree), stream(getCurrentPath())) + .filter(t -> t instanceof ExpressionStatementTree || t instanceof VariableTree) + .findFirst() + .ifPresent(fix::delete); + } + return super.scan(tree, null); + } + }.scan(state.getPath().getCompilationUnit(), null); + return fix.build(); + } + + private ImmutableMap findMocks(VisitorState state) { + Map mocks = new HashMap<>(); + AtomicBoolean injectMocks = new AtomicBoolean(false); + new SuppressibleTreePathScanner(state) { + @Override + public Void visitVariable(VariableTree tree, Void unused) { + VarSymbol symbol = getSymbol(tree); + if (INJECT_MOCKS_ANNOTATED.matches(tree, state)) { + injectMocks.set(true); + } + if (isEligible(symbol) + && (MOCK_OR_SPY_ANNOTATED.matches(tree, state) + || (tree.getInitializer() != null && MOCK.matches(tree.getInitializer(), state)))) { + mocks.put(symbol, tree); + } + return super.visitVariable(tree, null); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void unused) { + if (MOCK.matches(tree.getExpression(), state)) { + var symbol = getSymbol(tree.getVariable()); + if (isEligible(symbol)) { + mocks.put((VarSymbol) symbol, tree); + } + } + return super.visitAssignment(tree, null); + } + + private boolean isEligible(Symbol symbol) { + return symbol instanceof VarSymbol + && (!symbol.getKind().equals(FIELD) || canBeRemoved((VarSymbol) symbol)) + && annotatedAtMostMock(symbol); + } + + private boolean annotatedAtMostMock(Symbol symbol) { + return symbol.getAnnotationMirrors().stream() + .allMatch(a -> a.getAnnotationType().asElement().getSimpleName().contentEquals("Mock")); + } + }.scan(state.getPath().getCompilationUnit(), null); + // A bit hacky: but if we saw InjectMocks, just claim there are no potentially unused mocks. + return injectMocks.get() ? ImmutableMap.of() : ImmutableMap.copyOf(mocks); + } + + private static final Matcher MOCK = + anyOf( + staticMethod() + .onClass("org.mockito.Mockito") + .namedAnyOf("mock") + .withParameters("java.lang.Class"), + staticMethod().onClass("org.mockito.Mockito").namedAnyOf("spy")); + + private static final Matcher MOCK_OR_SPY_ANNOTATED = + anyOf(hasAnnotation("org.mockito.Mock"), hasAnnotation("org.mockito.Spy")); + + private static final Matcher INJECT_MOCKS_ANNOTATED = + hasAnnotation("org.mockito.InjectMocks"); + + private static final Matcher WHEN_OR_VERIFY = + anyMethod().anyClass().namedAnyOf("when", "verify"); +} 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 2cb9e525522..9d4dcbe3427 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -224,6 +224,7 @@ import com.google.errorprone.bugpatterns.MixedArrayDimensions; import com.google.errorprone.bugpatterns.MixedDescriptors; import com.google.errorprone.bugpatterns.MixedMutabilityReturnType; +import com.google.errorprone.bugpatterns.MockNotUsedInProduction; import com.google.errorprone.bugpatterns.MockitoUsage; import com.google.errorprone.bugpatterns.ModifiedButNotUsed; import com.google.errorprone.bugpatterns.ModifyCollectionInEnhancedForLoop; @@ -911,6 +912,7 @@ public static ScannerSupplier errorChecks() { MissingOverride.class, MissingSummary.class, MixedMutabilityReturnType.class, + MockNotUsedInProduction.class, ModifiedButNotUsed.class, ModifyCollectionInEnhancedForLoop.class, ModifySourceCollectionInStream.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java new file mode 100644 index 00000000000..97214fe4b91 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/MockNotUsedInProductionTest.java @@ -0,0 +1,232 @@ +/* + * 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.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class MockNotUsedInProductionTest { + private final CompilationTestHelper helper = + CompilationTestHelper.newInstance(MockNotUsedInProduction.class, getClass()); + + private final BugCheckerRefactoringTestHelper refactoring = + BugCheckerRefactoringTestHelper.newInstance(MockNotUsedInProduction.class, getClass()); + + @Test + public void neverUsed() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " // BUG: Diagnostic contains:", + " Test test = mock(Test.class);", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void spyNeverUsed() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.spy;", + "import static org.mockito.Mockito.verify;", + "class Test {", + " public Object test() {", + " // BUG: Diagnostic contains:", + " Test test = spy(new Test());", + " verify(test).test();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void passedToProduction() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " when(test.test()).thenCallRealMethod();", + " return test.test();", + " }", + "}") + .doTest(); + } + + @Test + public void possiblyBound() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Bind @Mock public Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void publicField() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock public Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void qualifiedWithThis_stillSeen() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " public Test test() {", + " return this.test;", + " }", + "}") + .doTest(); + } + + @Test + public void privateField() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " // BUG: Diagnostic contains:", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void injectMocks_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.InjectMocks;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " @InjectMocks Test t;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void suppressionWorks() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @SuppressWarnings(\"MockNotUsedInProduction\")", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void refactoring() { + refactoring + .addInputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " @Mock private Test test;", + " public Object test() {", + " when(test.test()).thenCallRealMethod();", + " return null;", + " }", + "}") + .addOutputLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.when;", + "import com.google.inject.testing.fieldbinder.Bind;", + "import org.mockito.Mock;", + "class Test {", + " public Object test() {", + " return null;", + " }", + "}") + .doTest(); + } +} From 1d1a896e6349a73ac04fddd30b07e7a79c71037c Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Mon, 11 Jul 2022 11:59:01 -0700 Subject: [PATCH 065/102] Turn `CanIgnoreReturnValueSuggester` into a warning. #checkreturnvalue PiperOrigin-RevId: 460267736 --- .../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 9d4dcbe3427..f45f1a6101c 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -802,6 +802,7 @@ public static ScannerSupplier errorChecks() { BugPatternNaming.class, ByteBufferBackingArray.class, CacheLoaderNull.class, + CanIgnoreReturnValueSuggester.class, CanonicalDuration.class, CatchAndPrintStackTrace.class, CatchFail.class, @@ -1027,7 +1028,6 @@ public static ScannerSupplier errorChecks() { BindingToUnqualifiedCommonType.class, BooleanParameter.class, BuilderReturnThis.class, - CanIgnoreReturnValueSuggester.class, CannotMockFinalClass.class, CannotMockFinalMethod.class, CatchingUnchecked.class, From 3dd2abc0fe933a396d888db5d2f0dfd1fda4ff5e Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 11 Jul 2022 15:31:56 -0700 Subject: [PATCH 066/102] Add a note about code excerpts PiperOrigin-RevId: 460314772 --- docs/bugpattern/javadoc/InvalidInlineTag.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/bugpattern/javadoc/InvalidInlineTag.md b/docs/bugpattern/javadoc/InvalidInlineTag.md index 97d0da6cf8e..9cba566ed68 100644 --- a/docs/bugpattern/javadoc/InvalidInlineTag.md +++ b/docs/bugpattern/javadoc/InvalidInlineTag.md @@ -21,6 +21,24 @@ int twoTimes(int n) { } ``` +If the `@` symbol occurrs inside a code excerpt, the fix is to escape the code +excerpt using `

{@code ... }
`: + +```java +/** + * Summary fragment. + * + *
{@code
+ * Your code here.
+ * Can include .
+ * You can even include snippets that contain annotations, e.g.:
+ * @Override public String toString() { ... }
+ * }
+ * + *

Following paragraph. + */ +``` + ## Suppression Suppress by applying `@SuppressWarnings("InvalidInlineTag")` to the element From ae501e6506ef143f92ff322546f1cc0890fdfcd4 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 12 Jul 2022 03:25:33 -0700 Subject: [PATCH 067/102] Complain about @Mock and @Inject being combined. PiperOrigin-RevId: 460419082 --- .../bugpatterns/UnnecessaryAssignment.java | 14 ++++++++++++-- .../bugpatterns/UnnecessaryAssignmentTest.java | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java index 9e34317586a..7ea5f1d9afe 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryAssignment.java @@ -121,13 +121,23 @@ public Description matchAssignment(AssignmentTree tree, VisitorState state) { @Override public Description matchVariable(VariableTree tree, VisitorState state) { + boolean hasMockAnnotation = HAS_MOCK_ANNOTATION.matches(tree, state); + boolean hasInjectyAnnotation = HAS_NON_MOCK_FRAMEWORK_ANNOTATION.matches(tree, state); + if (hasMockAnnotation && hasInjectyAnnotation) { + return buildDescription(tree) + .setMessage( + "Fields shouldn't be annotated with both @Mock and another @Inject-like annotation," + + " because both Mockito and the injector will assign to the field, and one of" + + " the values will overwrite the other") + .build(); + } if (tree.getInitializer() == null) { return NO_MATCH; } - if (HAS_MOCK_ANNOTATION.matches(tree, state)) { + if (hasMockAnnotation) { return describeMatch(tree, createMockFix(tree, state)); } - if (HAS_NON_MOCK_FRAMEWORK_ANNOTATION.matches(tree, state)) { + if (hasInjectyAnnotation) { Description.Builder description = buildDescription(tree); if (!tree.getModifiers().getFlags().contains(Modifier.FINAL)) { String source = diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java index ef7210b2b72..9bbe767d6bb 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/UnnecessaryAssignmentTest.java @@ -56,6 +56,20 @@ public void negative() { .doTest(); } + @Test + public void doubleAnnotation() { + testHelper + .addSourceLines( + "Test.java", // + "import com.google.inject.Inject;", + "import org.mockito.Mock;", + "class Test {", + " // BUG: Diagnostic contains: both", + " @Mock @Inject Object mockObject;", + "}") + .doTest(); + } + @Test public void refactoring() { refactoringHelper From 486c8796e76ec220841320657e16bf6fac5e6ad8 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Wed, 13 Jul 2022 06:23:24 -0700 Subject: [PATCH 068/102] Allow the external CIRV list to be "disabled" by being set to a blank value. PiperOrigin-RevId: 460698846 --- .../checkreturnvalue/ExternalCanIgnoreReturnValue.java | 1 + 1 file changed, 1 insertion(+) 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 cf306796b3b..b83389beef4 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 @@ -62,6 +62,7 @@ private ExternalCanIgnoreReturnValue() {} .errorProneOptions() .getFlags() .get(EXTERNAL_API_EXCLUSION_LIST) + .filter(s -> !s.isEmpty()) .map( filename -> loadConfigListFromFile(filename, state.errorProneOptions().getFlags())) From d6ee78c4b3f499cca7e1ee17fa8f3030601a2058 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 13 Jul 2022 07:43:00 -0700 Subject: [PATCH 069/102] Produce errors for accidental calls to `getClass()` on more types. I could imagine doing this for yet more types, but I stopped here. More candidates: ``` $ grep -oP '(?<=#)get[^(]*Class([A-Z][^(]*)?(?=[(])' G-Index\ \(Java\ SE\ 18\ \&\ JDK\ 18\).html | sort -u | grep -v -e ClassPath -e ClassLoader getAncestorOfClass getArgumentClass getBeanClass getCallerClass getCapturingClass getClass getClassAnnotation getClassBody getClassContext getClassGuard getClassLoadingLock getClassLoadingMXBean getClassName getColumnClass getColumnClassName getCredentialClass getCrossPlatformLookAndFeelClassName getCustomizerClass getDataClass getDeclaringClass getDefaultRepresentationClass getDefaultRepresentationClassAsString getDefinitionClass getDefinitionClassFile getEditorKitClassNameForContentType getEnclosingClass getExceptionClassName getFactoryClassLocation getFactoryClassName getFunctionalInterfaceClass getImplClass getImplementationClass getInputClass getLineClass getLinkerForClass getLoadedClassCount getObjectClass getObjectStreamClass getOutputClass getParameterClassName getPluginClassName getProfileClass getPropertyEditorClass getProxyClass getRefClass getRefMBeanClassName getRepresentationClass getRepresentationClassName getRepresentedClass getSchemaClassDefinition getServiceClass getServiceProviderByClass getSourceClassName getSystemLookAndFeelClassName getTotalLoadedClassCount getTrafficClass getTranslatorClass getUIClass getUIClassID getUIClassNamesForRole getUnloadedClassCount getValueClass ``` PiperOrigin-RevId: 460712498 --- .../bugpatterns/DoNotCallChecker.java | 58 ++++++++++ .../bugpatterns/DoNotCallCheckerTest.java | 107 ++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java index a92c06de47b..ac30960379f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; @@ -66,6 +67,18 @@ @BugPattern(name = "DoNotCall", summary = "This method should not be called.", severity = ERROR) public class DoNotCallChecker extends BugChecker implements MethodTreeMatcher, CompilationUnitTreeMatcher { + private final boolean checkNewGetClassMethods; + + public DoNotCallChecker(ErrorProneFlags flags) { + checkNewGetClassMethods = + flags.getBoolean("DoNotCallChecker:CheckNewGetClassMethods").orElse(true); + } + + private static final Matcher STACK_TRACE_ELEMENT_GET_CLASS = + instanceMethod().onExactClass("java.lang.StackTraceElement").named("getClass"); + + private static final Matcher ANY_GET_CLASS = + instanceMethod().anyClass().named("getClass"); // If your method cannot be annotated with @DoNotCall (e.g., it's a JDK or thirdparty method), // then add it to this Map with an explanation. @@ -133,6 +146,46 @@ public class DoNotCallChecker extends BugChecker "Calling getClass on StackTraceElement returns the Class object for" + " StackTraceElement, you probably meant to retrieve the class containing the" + " execution point represented by this stack trace element using getClassName") + .put( + instanceMethod().onExactClass("java.lang.StackWalker").named("getClass"), + "Calling getClass on StackWalker returns the Class object for StackWalker, you" + + " probably meant to retrieve the class containing the execution point" + + " represented by this StackWalker using getCallerClass") + .put( + instanceMethod().onExactClass("java.lang.StackWalker$StackFrame").named("getClass"), + "Calling getClass on StackFrame returns the Class object for StackFrame, you probably" + + " meant to retrieve the class containing the execution point represented by" + + " this StackFrame using getClassName") + .put( + instanceMethod().onExactClass("java.lang.reflect.Constructor").named("getClass"), + "Calling getClass on Constructor returns the Class object for Constructor, you" + + " probably meant to retrieve the class containing the constructor represented" + + " by this Constructor using getDeclaringClass") + .put( + instanceMethod().onExactClass("java.lang.reflect.Field").named("getClass"), + "Calling getClass on Field returns the Class object for Field, you probably meant to" + + " retrieve the class containing the field represented by this Field using" + + " getDeclaringClass") + .put( + instanceMethod().onExactClass("java.lang.reflect.Method").named("getClass"), + "Calling getClass on Method returns the Class object for Method, you probably meant" + + " to retrieve the class containing the method represented by this Method using" + + " getDeclaringClass") + .put( + instanceMethod().onExactClass("java.beans.BeanDescriptor").named("getClass"), + "Calling getClass on BeanDescriptor returns the Class object for BeanDescriptor, you" + + " probably meant to retrieve the class described by this BeanDescriptor using" + + " getBeanClass") + .put( + /* + * LockInfo has a publicly visible subclass, MonitorInfo. It seems unlikely that + * anyone is using getClass() in an attempt to distinguish the two. (If anyone is, + * then it would make more sense to use instanceof, anyway.) + */ + instanceMethod().onDescendantOf("java.lang.management.LockInfo").named("getClass"), + "Calling getClass on LockInfo returns the Class object for LockInfo, you probably" + + " meant to retrieve the class of the object that is being locked using" + + " getClassName") .buildOrThrow(); static final String DO_NOT_CALL = "com.google.errorprone.annotations.DoNotCall"; @@ -201,6 +254,11 @@ public Void visitMemberReference(MemberReferenceTree tree, Void unused) { private void handleTree(ExpressionTree tree, MethodSymbol symbol) { for (Map.Entry, String> matcher : THIRD_PARTY_METHODS.entrySet()) { if (matcher.getKey().matches(tree, state)) { + if (!checkNewGetClassMethods + && ANY_GET_CLASS.matches(tree, state) + && !STACK_TRACE_ELEMENT_GET_CLASS.matches(tree, state)) { + return; + } state.reportMatch(buildDescription(tree).setMessage(matcher.getValue()).build()); return; } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java index b80ffb765d1..8215b323bbe 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java @@ -552,4 +552,111 @@ public void positive_getSimpleName_refactoredToGetClassName() { "}") .doTest(); } + + @Test + public void positive_stackWalkerGetClass() { + testHelper + .addSourceLines( + "Test.java", + "class Test{", + " void f(StackWalker w) {", + " // BUG: Diagnostic contains: getCallerClass", + " w.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_stackFrameGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.StackWalker.StackFrame;", + "class Test{", + " void f(StackFrame f) {", + " // BUG: Diagnostic contains: getClassName", + " f.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_constructorGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Constructor;", + "class Test{", + " void f(Constructor c) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " c.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_fieldGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Field;", + "class Test{", + " void f(Field f) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " f.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_methodGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.Method;", + "class Test{", + " void f(Method m) {", + " // BUG: Diagnostic contains: getDeclaringClass", + " m.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_beanDescriptorGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.beans.BeanDescriptor;", + "class Test{", + " void f(BeanDescriptor b) {", + " // BUG: Diagnostic contains: getBeanClass", + " b.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_lockInfoGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.management.LockInfo;", + "import java.lang.management.MonitorInfo;", + "class Test{", + " void f(LockInfo l, MonitorInfo m) {", + " // BUG: Diagnostic contains: getClassName", + " l.getClass();", + " // BUG: Diagnostic contains: getClassName", + " m.getClass();", + " }", + "}") + .doTest(); + } } From 49e4b34998e8b23e8e076feed96311983b56c624 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 13 Jul 2022 11:59:55 -0700 Subject: [PATCH 070/102] Don't suggest `@Nullable` on implementations of methods like `Map.put` if those implementations always throw or they are `@DoNotCall`. Also, provide a different error message for this "implementations of well-known methods" case, one that is different from the usual "Method returns a definitely null value" message. (I considered continuing the make the suggestion in "aggressive" mode, but I'm not sure if it's worth the trouble even then, since it may lead to a discussion every time the suggestion appears.) Fixes https://github.com/google/error-prone/issues/2910 PiperOrigin-RevId: 460772102 --- .../nullness/ReturnMissingNullable.java | 21 ++++++++++++- .../nullness/ReturnMissingNullableTest.java | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) 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 bb557cc181f..feff9fa1bd4 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 @@ -35,9 +35,11 @@ import static com.google.errorprone.util.ASTHelpers.constValue; import static com.google.errorprone.util.ASTHelpers.findEnclosingMethod; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; import static com.google.errorprone.util.ASTHelpers.methodCanBeOverridden; import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; +import static com.sun.source.tree.Tree.Kind.THROW; import static java.lang.Boolean.FALSE; import static java.util.regex.Pattern.compile; import static javax.lang.model.type.TypeKind.TYPEVAR; @@ -280,6 +282,17 @@ void doVisitMethod(MethodTree tree) { return; } + if (tree.getBody() != null + && tree.getBody().getStatements().size() == 1 + && getOnlyElement(tree.getBody().getStatements()).getKind() == THROW) { + return; + } + + if (hasAnnotation( + tree, "com.google.errorprone.annotations.DoNotCall", stateForCompilationUnit)) { + return; + } + for (MethodSymbol methodKnownToReturnNull : METHODS_KNOWN_TO_RETURN_NULL.get(stateForCompilationUnit)) { if (stateForCompilationUnit @@ -289,7 +302,13 @@ void doVisitMethod(MethodTree tree) { fixByAddingNullableAnnotationToReturnType( stateForCompilationUnit.withPath(getCurrentPath()), tree); if (!fix.isEmpty()) { - stateForCompilationUnit.reportMatch(describeMatch(tree, fix)); + stateForCompilationUnit.reportMatch( + buildDescription(tree) + .setMessage( + "Nearly all implementations of this method must return null, but it is" + + " not annotated @Nullable") + .addFix(fix) + .build()); } } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java index df2d5e4c575..64592ffda59 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/ReturnMissingNullableTest.java @@ -574,6 +574,36 @@ public void testImplementsMap() { .doTest(); } + @Test + public void testImplementsMapButAlwaysThrows() { + createCompilationTestHelper() + .addSourceLines( + "MyMap.java", + "import java.util.Map;", + "abstract class MyMap implements Map {", + " @Override", + " public V put(K k, V v) {", + " throw new UnsupportedOperationException();", + " }", + "}") + .doTest(); + } + + @Test + public void testImplementsMapButDoNotCall() { + createCompilationTestHelper() + .addSourceLines( + "MyMap.java", + "import com.google.errorprone.annotations.DoNotCall;", + "import java.util.Map;", + "interface MyMap extends Map {", + " @DoNotCall", + " @Override", + " V put(K k, V v);", + "}") + .doTest(); + } + @Test public void testOnlyIfAlreadyInScopeAndItIs() { createCompilationTestHelper() From 25d1c005aca932cab500ef40aa98ad671577c468 Mon Sep 17 00:00:00 2001 From: ghm Date: Thu, 14 Jul 2022 03:19:36 -0700 Subject: [PATCH 071/102] Make a defensive null check in SameNameButDifferent. Fixes(?) external #3245. (I haven't included a test as the external bug doesn't have a repro, and is apparently in generated code: which makes me assume something odd is going on.) PiperOrigin-RevId: 460918647 --- .../google/errorprone/bugpatterns/SameNameButDifferent.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java index 5ccba6f30ec..d854a712f47 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java @@ -183,6 +183,9 @@ private static Optional getBetterImport(TypeSymbol classSymbol, String s Symbol owner = classSymbol; long dots = simpleName.chars().filter(c -> c == '.').count(); for (long i = 0; i < dots + 1; ++i) { + if (owner == null) { + return Optional.empty(); + } owner = owner.owner; } if (owner instanceof ClassSymbol) { From 0da434efc8adca8682bea159c97f7fab65745c83 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Fri, 15 Jul 2022 19:08:06 -0700 Subject: [PATCH 072/102] Avoid relying on source positions in `SameNameButDifferent` PiperOrigin-RevId: 461283970 --- .../bugpatterns/SameNameButDifferent.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java index d854a712f47..5163b07d59e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/SameNameButDifferent.java @@ -41,6 +41,7 @@ import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.TypeSymbol; +import com.sun.tools.javac.util.Position; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -48,6 +49,8 @@ import java.util.Map; import java.util.Optional; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import org.checkerframework.checker.nullness.qual.Nullable; /** Looks for types being shadowed by other types in a way that may be confusing. */ @BugPattern( @@ -92,21 +95,38 @@ private boolean shouldIgnore() { && getSymbol(parentTree) instanceof ClassSymbol; } + private @Nullable String qualifiedName(Tree tree) { + if (state.getEndPosition(tree) == Position.NOPOS) { + return null; + } + ArrayDeque parts = new ArrayDeque<>(); + while (tree instanceof MemberSelectTree) { + MemberSelectTree select = (MemberSelectTree) tree; + parts.addFirst(select.getIdentifier()); + tree = select.getExpression(); + } + if (!(tree instanceof IdentifierTree)) { + return null; + } + parts.addFirst(((IdentifierTree) tree).getName()); + return Joiner.on('.').join(parts); + } + private void handle(Tree tree) { if (tree instanceof IdentifierTree && ((IdentifierTree) tree).getName().contentEquals("Builder")) { return; } - String treeSource = state.getSourceForNode(tree); - if (treeSource == null) { + String qualifiedName = qualifiedName(tree); + if (qualifiedName == null) { return; } Symbol symbol = getSymbol(tree); if (symbol instanceof ClassSymbol) { - List treePaths = table.get(treeSource, symbol.type.tsym); + List treePaths = table.get(qualifiedName, symbol.type.tsym); if (treePaths == null) { treePaths = new ArrayList<>(); - table.put(treeSource, symbol.type.tsym, treePaths); + table.put(qualifiedName, symbol.type.tsym, treePaths); } treePaths.add(getCurrentPath()); } From 5fc5d9899eca884f082a034d5dc1401186112b85 Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 18 Jul 2022 05:51:52 -0700 Subject: [PATCH 073/102] Rephrase the description in MockNotUsedInProduction. PiperOrigin-RevId: 461596122 --- .../errorprone/bugpatterns/MockNotUsedInProduction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java index 4baa65b6ce3..e7a32649353 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MockNotUsedInProduction.java @@ -59,8 +59,8 @@ @BugPattern( severity = WARNING, summary = - "This mock is configured but never escapes to be used in production code. Should it be" - + " removed?") + "This mock is instantiated and configured, but is never passed to production code. It" + + " should be either removed or used.") public final class MockNotUsedInProduction extends BugChecker implements CompilationUnitTreeMatcher { @Override From 1b98f3232e4ea48fe28f4adff4d55ee91565cec8 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Mon, 18 Jul 2022 12:08:09 -0700 Subject: [PATCH 074/102] Avoid re-compiling the same Pattern over and over in InvalidInlineTag We only have 3 "modes", and each one needs two Patterns. I extracted a 3-element enum, each storing two Patterns. PiperOrigin-RevId: 461680760 --- .../bugpatterns/javadoc/InvalidInlineTag.java | 71 ++++++++++--------- .../bugpatterns/javadoc/JavadocTag.java | 2 + 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java index 2523d727a3c..95eed6453f9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/InvalidInlineTag.java @@ -74,26 +74,42 @@ public final class InvalidInlineTag extends BugChecker private static final Splitter DOT_SPLITTER = Splitter.on('.'); + private void scanTags( + VisitorState state, Context context, ImmutableSet parameters, DocTreePath path) { + new InvalidTagChecker(state, context, parameters).scan(path, null); + } + + private enum Context { + CLASS(JavadocTag.VALID_CLASS_TAGS), + METHOD(JavadocTag.VALID_METHOD_TAGS), + VARIABLE(JavadocTag.VALID_VARIABLE_TAGS); + + final ImmutableSet validTags; + final Pattern misplacedCurly; + final Pattern parensRatherThanCurly; + + Context(ImmutableSet validTags) { + this.validTags = validTags; + String validInlineTags = + validTags.stream() + .filter(tag -> tag.type() == TagType.INLINE) + .map(JavadocTag::name) + .collect(joining("|")); + this.misplacedCurly = Pattern.compile(String.format("@(%s)\\{", validInlineTags)); + this.parensRatherThanCurly = Pattern.compile(String.format("\\(@(%s)", validInlineTags)); + } + } + @Override public Description matchClass(ClassTree classTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); if (path != null) { ImmutableSet parameters = ImmutableSet.of(); - scanTags(state, JavadocTag.VALID_CLASS_TAGS, parameters, path); + scanTags(state, Context.CLASS, parameters, path); } return Description.NO_MATCH; } - private void scanTags( - VisitorState state, - ImmutableSet tags, - ImmutableSet parameters, - DocTreePath path) { - try (InvalidTagChecker checker = new InvalidTagChecker(state, tags, parameters)) { - checker.scan(path, null); - } - } - @Override public Description matchMethod(MethodTree methodTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); @@ -102,7 +118,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { methodTree.getParameters().stream() .map(v -> v.getName().toString()) .collect(toImmutableSet()); - scanTags(state, JavadocTag.VALID_METHOD_TAGS, parameters, path); + scanTags(state, Context.METHOD, parameters, path); } return Description.NO_MATCH; } @@ -111,7 +127,7 @@ public Description matchMethod(MethodTree methodTree, VisitorState state) { public Description matchVariable(VariableTree variableTree, VisitorState state) { DocTreePath path = Utils.getDocTreePath(state); if (path != null) { - scanTags(state, JavadocTag.VALID_VARIABLE_TAGS, /* parameters= */ ImmutableSet.of(), path); + scanTags(state, Context.VARIABLE, /* parameters= */ ImmutableSet.of(), path); } return Description.NO_MATCH; } @@ -123,29 +139,19 @@ static String getMessageForInvalidTag(String paramName) { paramName); } - final class InvalidTagChecker extends DocTreePathScanner implements AutoCloseable { + final class InvalidTagChecker extends DocTreePathScanner { private final VisitorState state; - private final ImmutableSet validTags; private final ImmutableSet parameters; - - private final Pattern misplacedCurly; - private final Pattern parensRatherThanCurly; + private final Context context; private final Set fixedTags = new HashSet<>(); private InvalidTagChecker( - VisitorState state, ImmutableSet validTags, ImmutableSet parameters) { + VisitorState state, Context context, ImmutableSet parameters) { this.state = state; - this.validTags = validTags; + this.context = context; this.parameters = parameters; - String validInlineTags = - validTags.stream() - .filter(tag -> tag.type() == TagType.INLINE) - .map(JavadocTag::name) - .collect(joining("|")); - this.misplacedCurly = Pattern.compile(String.format("@(%s)\\{", validInlineTags)); - this.parensRatherThanCurly = Pattern.compile(String.format("\\(@(%s)", validInlineTags)); } @Override @@ -180,7 +186,7 @@ public Void visitText(TextTree node, Void unused) { private void handleMalformedTags(TextTree node) { String body = node.getBody(); - Matcher matcher = misplacedCurly.matcher(body); + Matcher matcher = context.misplacedCurly.matcher(body); Comment comment = ((DCDocComment) getCurrentPath().getDocComment()).comment; while (matcher.find()) { int beforeAt = comment.getSourcePos(((DCText) node).pos + matcher.start()); @@ -198,7 +204,7 @@ private void handleMalformedTags(TextTree node) { private void handleIncorrectParens(TextTree node) { String body = node.getBody(); - Matcher matcher = parensRatherThanCurly.matcher(body); + Matcher matcher = context.parensRatherThanCurly.matcher(body); Comment comment = ((DCDocComment) getCurrentPath().getDocComment()).comment; while (matcher.find()) { int beforeAt = comment.getSourcePos(((DCText) node).pos + matcher.start()); @@ -336,7 +342,7 @@ private void reportUnknownTag(DocTree docTree, JavadocTag tag) { Utils.getBestMatch( tag.name(), /* maxEditDistance= */ 2, - validTags.stream() + context.validTags.stream() .filter(t -> t.type().equals(tag.type())) .map(JavadocTag::name) .collect(toImmutableSet())); @@ -371,7 +377,7 @@ public Void scan(DocTree docTree, Void unused) { return null; } JavadocTag tag = inlineTag(((DCInlineTag) docTree).getTagName()); - if (validTags.contains(tag) || JavadocTag.KNOWN_OTHER_TAGS.contains(tag)) { + if (context.validTags.contains(tag) || JavadocTag.KNOWN_OTHER_TAGS.contains(tag)) { return null; } String message = @@ -383,8 +389,5 @@ public Void scan(DocTree docTree, Void unused) { .build()); return null; } - - @Override - public void close() {} } } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java index ae69ff70a4f..d0f91b89904 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/JavadocTag.java @@ -20,9 +20,11 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.Immutable; import java.util.stream.Stream; /** Describes Javadoc tags, and contains lists of valid tags. */ +@Immutable @AutoValue abstract class JavadocTag { From 5f4cb19767bd7d73d03aa5292b8f9371c6533a9b Mon Sep 17 00:00:00 2001 From: cpovirk Date: Mon, 18 Jul 2022 21:50:01 -0700 Subject: [PATCH 075/102] Produce errors for accidental calls to `getClass()` on yet more types. followup to https://github.com/google/error-prone/commit/d6ee78c4b3f499cca7e1ee17fa8f3030601a2058 -- might as well get these covered by the same flag flip PiperOrigin-RevId: 461794698 --- .../bugpatterns/DoNotCallChecker.java | 30 +++++++++++++ .../bugpatterns/DoNotCallCheckerTest.java | 45 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java index ac30960379f..e1bcf628f2f 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -171,6 +171,13 @@ public DoNotCallChecker(ErrorProneFlags flags) { "Calling getClass on Method returns the Class object for Method, you probably meant" + " to retrieve the class containing the method represented by this Method using" + " getDeclaringClass") + .put( + instanceMethod() + .onExactClass("java.lang.reflect.ParameterizedType") + .named("getClass"), + "Calling getClass on ParameterizedType returns the Class object for" + + " ParameterizedType, you probably meant to retrieve the class containing the" + + " method represented by this ParameterizedType using getRawType") .put( instanceMethod().onExactClass("java.beans.BeanDescriptor").named("getClass"), "Calling getClass on BeanDescriptor returns the Class object for BeanDescriptor, you" @@ -186,6 +193,29 @@ public DoNotCallChecker(ErrorProneFlags flags) { "Calling getClass on LockInfo returns the Class object for LockInfo, you probably" + " meant to retrieve the class of the object that is being locked using" + " getClassName") + /* + * These methods are part of Guava, but we have to list them in this "thirdparty" section: + * We can't annotate them with @DoNotCall because we can't override getClass. + */ + .put( + instanceMethod() + .onExactClass("com.google.common.reflect.ClassPath$ClassInfo") + .named("getClass"), + "Calling getClass on ClassInfo returns the Class object for ClassInfo, you probably" + + " meant to retrieve the class described by this ClassInfo using getName or" + + " load") + /* + * Users of TypeToken have to create a subclass. The static type of their instance is + * probably often still "TypeToken," but that may change as we see more usage of `var`. So + * let's check subclasses, too. If anyone defines an overload of getClass on such a + * subclass, this check will give that person a bad time in one additional way. + */ + .put( + instanceMethod() + .onDescendantOf("com.google.common.reflect.TypeToken") + .named("getClass"), + "Calling getClass on TypeToken returns the Class object for TypeToken, you probably" + + " meant to retrieve the class described by this TypeToken using getRawType") .buildOrThrow(); static final String DO_NOT_CALL = "com.google.errorprone.annotations.DoNotCall"; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java index 8215b323bbe..04b7e64eb16 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java @@ -659,4 +659,49 @@ public void positive_lockInfoGetClass() { "}") .doTest(); } + + @Test + public void positive_parameterizedTypeGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import java.lang.reflect.ParameterizedType;", + "class Test{", + " void f(ParameterizedType t) {", + " // BUG: Diagnostic contains: getRawType", + " t.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_classInfoGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import com.google.common.reflect.ClassPath.ClassInfo;", + "class Test{", + " void f(ClassInfo i) {", + " // BUG: Diagnostic contains: getName", + " i.getClass();", + " }", + "}") + .doTest(); + } + + @Test + public void positive_typeTokenGetClass() { + testHelper + .addSourceLines( + "Test.java", + "import com.google.common.reflect.TypeToken;", + "class Test{", + " void f(TypeToken t) {", + " // BUG: Diagnostic contains: getRawType", + " t.getClass();", + " }", + "}") + .doTest(); + } } From 7d1930eccb6e4f41154289a1ca13d0d223535194 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 19 Jul 2022 03:23:47 -0700 Subject: [PATCH 076/102] Also handle doCallRealMethod. PiperOrigin-RevId: 461840629 --- .../bugpatterns/DirectInvocationOnMock.java | 15 +++++++++++++++ .../bugpatterns/DirectInvocationOnMockTest.java | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java index 26ce3ea9df5..3ba71e003ab 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -20,8 +20,10 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.fixes.SuggestedFixes.qualifyStaticImport; import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.matchers.Matchers.allOf; import static com.google.errorprone.matchers.Matchers.anyMethod; import static com.google.errorprone.matchers.Matchers.instanceMethod; +import static com.google.errorprone.matchers.Matchers.receiverOfInvocation; import static com.google.errorprone.matchers.Matchers.staticMethod; import static com.google.errorprone.util.ASTHelpers.getReceiver; import static com.google.errorprone.util.ASTHelpers.getSymbol; @@ -76,6 +78,13 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { } return super.visitMethodInvocation(tree, null); } + if (DO_CALL_REAL_METHOD.matches(tree, state)) { + var methodSymbol = getSymbol(getCurrentPath().getParentPath().getParentPath().getLeaf()); + if (methodSymbol instanceof MethodSymbol) { + methodsCallingRealImplementations.add((MethodSymbol) methodSymbol); + } + return super.visitMethodInvocation(tree, null); + } if (methodsCallingRealImplementations.contains(getSymbol(tree))) { return super.visitMethodInvocation(tree, null); } @@ -154,6 +163,12 @@ public Void visitAssignment(AssignmentTree tree, Void unused) { private static final Matcher MOCK = staticMethod().onClass("org.mockito.Mockito").named("mock").withParameters("java.lang.Class"); + private static final Matcher DO_CALL_REAL_METHOD = + allOf( + instanceMethod().onDescendantOf("org.mockito.stubbing.Stubber").named("when"), + receiverOfInvocation( + staticMethod().onClass("org.mockito.Mockito").named("doCallRealMethod"))); + private static final Matcher WHEN = anyMethod().anyClass().named("when"); private static final Matcher THEN_CALL_REAL_METHOD = diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java index 1fe2822797e..8c07fd2277a 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -172,6 +172,23 @@ public void directInvocationOnMock_setUpToCallRealMethod_noFinding() { .doTest(); } + @Test + public void directInvocationOnMock_setUpWithDoCallRealMethod_noFinding() { + helper + .addSourceLines( + "Test.java", + "import static org.mockito.Mockito.mock;", + "import static org.mockito.Mockito.doCallRealMethod;", + "class Test {", + " public Object test() {", + " Test test = mock(Test.class);", + " doCallRealMethod().when(test).test();", + " return test.test();", + " }", + "}") + .doTest(); + } + @Test public void directInvocationOnMock_withinCustomWhen_noFinding() { helper From f427ab825a3e03b3374f2c40b736e5ca9a595ca4 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 19 Jul 2022 15:31:57 -0700 Subject: [PATCH 077/102] Treat `Thread.run` as `@DoNoCall`. Callers often want `Thread.start`. Yes, some do mean to run the work directly. But such code should probably already have a comment, so a suppression doesn't seem like a significant additional burden. And I assume that we're seeing a lot of survivor bias when we search for existing occurrences. PiperOrigin-RevId: 461991043 --- .../bugpatterns/DoNotCallChecker.java | 45 +++++++++++++++++++ .../bugpatterns/DoNotCallCheckerTest.java | 27 +++++++++++ 2 files changed, 72 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java index e1bcf628f2f..cbacb2e14dc 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotCallChecker.java @@ -19,6 +19,8 @@ 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.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.not; 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.findSuperMethods; @@ -28,6 +30,9 @@ import static com.google.errorprone.util.ASTHelpers.hasAnnotation; import static com.google.errorprone.util.ASTHelpers.isConsideredFinal; import static com.google.errorprone.util.ASTHelpers.isSameType; +import static com.sun.source.tree.Tree.Kind.IDENTIFIER; +import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; +import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -46,7 +51,9 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; @@ -68,10 +75,12 @@ public class DoNotCallChecker extends BugChecker implements MethodTreeMatcher, CompilationUnitTreeMatcher { private final boolean checkNewGetClassMethods; + private final boolean checkThreadRun; public DoNotCallChecker(ErrorProneFlags flags) { checkNewGetClassMethods = flags.getBoolean("DoNotCallChecker:CheckNewGetClassMethods").orElse(true); + checkThreadRun = flags.getBoolean("DoNotCallChecker:CheckThreadRun").orElse(true); } private static final Matcher STACK_TRACE_ELEMENT_GET_CLASS = @@ -80,6 +89,25 @@ public DoNotCallChecker(ErrorProneFlags flags) { private static final Matcher ANY_GET_CLASS = instanceMethod().anyClass().named("getClass"); + private static final Matcher THREAD_RUN = + instanceMethod().onDescendantOf("java.lang.Thread").named("run").withNoParameters(); + + private static final Matcher CALL_ON_SUPER = + (invocation, state) -> { + if (invocation.getKind() != METHOD_INVOCATION) { + return false; + } + ExpressionTree select = ((MethodInvocationTree) invocation).getMethodSelect(); + if (select.getKind() != MEMBER_SELECT) { + return false; + } + ExpressionTree receiver = ((MemberSelectTree) select).getExpression(); + if (receiver.getKind() != IDENTIFIER) { + return false; + } + return ((IdentifierTree) receiver).getName().contentEquals("super"); + }; + // If your method cannot be annotated with @DoNotCall (e.g., it's a JDK or thirdparty method), // then add it to this Map with an explanation. private static final ImmutableMap, String> THIRD_PARTY_METHODS = @@ -216,6 +244,20 @@ public DoNotCallChecker(ErrorProneFlags flags) { .named("getClass"), "Calling getClass on TypeToken returns the Class object for TypeToken, you probably" + " meant to retrieve the class described by this TypeToken using getRawType") + .put( + /* + * A call to super.run() from a direct subclass of Thread is a no-op. That could be + * worth telling the user about, but it's not as big a deal as "You meant to call + * start()," so we ignore it here. + * + * (And if someone defines a MyThread class with a run() method that does something, + * then a call to super.run() from a subclass of *MyThread* would *not* be a no-op, + * and we wouldn't want to flag it. Still, *usually* it's likely to be useful to + * report a warning even on subclasses of Thread, such as anonymous classes.) + */ + allOf(THREAD_RUN, not(CALL_ON_SUPER)), + "Calling run on Thread runs work on this thread, rather than the given thread, you" + + " probably meant to call start") .buildOrThrow(); static final String DO_NOT_CALL = "com.google.errorprone.annotations.DoNotCall"; @@ -289,6 +331,9 @@ private void handleTree(ExpressionTree tree, MethodSymbol symbol) { && !STACK_TRACE_ELEMENT_GET_CLASS.matches(tree, state)) { return; } + if (!checkThreadRun && THREAD_RUN.matches(tree, state)) { + return; + } state.reportMatch(buildDescription(tree).setMessage(matcher.getValue()).build()); return; } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java index 04b7e64eb16..5c811e456d0 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotCallCheckerTest.java @@ -704,4 +704,31 @@ public void positive_typeTokenGetClass() { "}") .doTest(); } + + @Test + public void positive_threadRun() { + testHelper + .addSourceLines( + "Test.java", + "class Test {", + " void f(Thread t) {", + " // BUG: Diagnostic contains: start", + " t.run();", + " }", + "}") + .doTest(); + } + + @Test + public void negative_threadSuperRun() { + testHelper + .addSourceLines( + "Test.java", + "class Test extends Thread {", + " @Override public void run() {", + " super.run();", + " }", + "}") + .doTest(); + } } From 81ed12469e30ec35da7766c29a35ccf8f04d83c7 Mon Sep 17 00:00:00 2001 From: ghm Date: Wed, 20 Jul 2022 05:59:46 -0700 Subject: [PATCH 078/102] Include the mock name in DirectInvocationOnMock findings. PiperOrigin-RevId: 462121823 --- .../errorprone/bugpatterns/DirectInvocationOnMock.java | 8 +++++++- .../bugpatterns/DirectInvocationOnMockTest.java | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java index 3ba71e003ab..b8c51c41206 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DirectInvocationOnMock.java @@ -101,7 +101,13 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) { if (isMock(receiver) && !(parent instanceof ExpressionTree && WHEN.matches((ExpressionTree) parent, state))) { - var description = buildDescription(tree); + var description = + buildDescription(tree) + .setMessage( + format( + "Methods should not be directly invoked on the mock `%s`. Should this be" + + " part of a verify(..) call?", + getSymbol(receiver).getSimpleName())); if (getCurrentPath().getParentPath().getLeaf() instanceof ExpressionStatementTree) { var fix = SuggestedFix.builder(); String verify = qualifyStaticImport("org.mockito.Mockito.verify", fix, state); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java index 8c07fd2277a..4799062d0e2 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DirectInvocationOnMockTest.java @@ -40,7 +40,7 @@ public void directInvocationOnMock() { "class Test {", " public void test() {", " Test test = mock(Test.class);", - " // BUG: Diagnostic contains:", + " // BUG: Diagnostic contains: test", " test.test();", " }", "}") From 85cc7f10eb42a51192d0ec6c56769b6ea52d2664 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Thu, 21 Jul 2022 13:46:10 -0700 Subject: [PATCH 079/102] Improve `CheckReturnValue` error messages: - Emphasize "not annotated `@CanIgnoreReturnValue`" over "annotated `@CheckReturnValue`." - List ways of making the error go away. - Attempt to explain how return values are ignored in the case of method references. - Optionally include more information about the API whose result is being ignored. PiperOrigin-RevId: 462461662 --- .../AbstractReturnValueIgnored.java | 12 +- .../bugpatterns/CheckReturnValue.java | 141 +++++++++++++++--- .../ExternalCanIgnoreReturnValue.java | 4 +- .../bugpatterns/CheckReturnValueTest.java | 50 +++---- .../CheckReturnValuePositiveCases.java | 49 +++++- 5 files changed, 200 insertions(+), 56 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java index f8d31064911..0d391209e0d 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/AbstractReturnValueIgnored.java @@ -201,6 +201,13 @@ protected boolean allowInExceptionThrowers() { */ protected Description describeReturnValueIgnored( MethodInvocationTree methodInvocationTree, VisitorState state) { + return buildDescription(methodInvocationTree) + .addFix(makeFix(methodInvocationTree, state)) + .setMessage(getMessage(getSymbol(methodInvocationTree).getSimpleName())) + .build(); + } + + final Fix makeFix(MethodInvocationTree methodInvocationTree, VisitorState state) { // Find the root of the field access chain, i.e. a.intern().trim() ==> a. ExpressionTree identifierExpr = ASTHelpers.getRootAssignable(methodInvocationTree); Type identifierType = null; @@ -236,10 +243,7 @@ protected Description describeReturnValueIgnored( fix = SuggestedFix.delete(parent); } } - return buildDescription(methodInvocationTree) - .addFix(fix) - .setMessage(getMessage(getSymbol(methodInvocationTree).getSimpleName())) - .build(); + return fix; } /** 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 e96de62dbf5..051f98534b1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -17,17 +17,22 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.bugpatterns.CheckReturnValue.MessageTrailerStyle.NONE; 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.ExternalCanIgnoreReturnValue.methodNameAndParams; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.surroundingClass; import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.mutableProtos; import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.protoBuilders; import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.EXPECTED; import static com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy.OPTIONAL; import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.globalDefault; import static com.google.errorprone.bugpatterns.checkreturnvalue.Rules.mapAnnotationSimpleName; +import static com.google.errorprone.fixes.SuggestedFix.emptyFix; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.getType; import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName; import com.google.common.collect.ImmutableMap; @@ -40,27 +45,32 @@ import com.google.errorprone.bugpatterns.checkreturnvalue.PackagesRule; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicy; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUsePolicyEvaluator; +import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberReferenceTree.ReferenceMode; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.MethodType; import java.util.Optional; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.Name; /** * @author eaftan@google.com (Eddie Aftandilian) */ @BugPattern( altNames = {"ResultOfMethodCallIgnored", "ReturnValueIgnored"}, - summary = "Ignored return value of method that is annotated with @CheckReturnValue", + summary = "The result of this call must be used", severity = ERROR) public class CheckReturnValue extends AbstractReturnValueIgnored implements MethodTreeMatcher, ClassTreeMatcher { @@ -73,12 +83,17 @@ public class CheckReturnValue extends AbstractReturnValueIgnored static final String CRV_PACKAGES = "CheckReturnValue:Packages"; + private final MessageTrailerStyle messageTrailerStyle; private final Optional constructorPolicy; private final Optional methodPolicy; private final ResultUsePolicyEvaluator evaluator; public CheckReturnValue(ErrorProneFlags flags) { super(flags); + this.messageTrailerStyle = + flags + .getEnum("CheckReturnValue:MessageTrailerStyle", MessageTrailerStyle.class) + .orElse(NONE); this.constructorPolicy = defaultPolicy(flags, CHECK_ALL_CONSTRUCTORS); this.methodPolicy = defaultPolicy(flags, CHECK_ALL_METHODS); @@ -237,25 +252,115 @@ && hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), CAN_IGNORE_RETU return Description.NO_MATCH; } + private Description describeInvocationResultIgnored( + Tree tree, + String shortCall, + String shortCallWithoutNew, + MethodSymbol symbol, + Fix fix, + VisitorState state) { + String message = + String.format( + "The result of `%s` must be used\n" + + "If you really don't want to use the result, then assign it to a variable:" + + " `var unused = ...`.\n" + + "\n" + + "If callers of `%s` shouldn't be required to use its result," + + " then annotate it with `@CanIgnoreReturnValue`.\n" + + "%s", + shortCall, shortCallWithoutNew, apiTrailer(symbol, state)); + return buildDescription(tree).addFix(fix).setMessage(message).build(); + } + @Override - protected String getMessage(Name name) { - return String.format( - 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); + protected Description describeReturnValueIgnored(MethodInvocationTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + String shortCall = symbol.name + (tree.getArguments().isEmpty() ? "()" : "(...)"); + String shortCallWithoutNew = shortCall; + return describeInvocationResultIgnored( + tree, shortCall, shortCallWithoutNew, symbol, makeFix(tree, state), state); } @Override - protected Description describeReturnValueIgnored(NewClassTree newClassTree, VisitorState state) { - return constructorPolicy.orElse(OPTIONAL).equals(EXPECTED) - ? buildDescription(newClassTree) - .setMessage( - String.format( - "Ignored return value of '%s', which wasn't annotated with" - + " @CanIgnoreReturnValue", - state.getSourceForNode(newClassTree.getIdentifier()))) - .build() - : super.describeReturnValueIgnored(newClassTree, state); + protected Description describeReturnValueIgnored(NewClassTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + String shortCallWithoutNew = + state.getSourceForNode(tree.getIdentifier()) + + (tree.getArguments().isEmpty() ? "()" : "(...)"); + String shortCall = "new " + shortCallWithoutNew; + return describeInvocationResultIgnored( + tree, shortCall, shortCallWithoutNew, symbol, emptyFix(), state); + } + + @Override + protected Description describeReturnValueIgnored(MemberReferenceTree tree, VisitorState state) { + MethodSymbol symbol = getSymbol(tree); + Type type = state.getTypes().memberType(getType(tree.getQualifierExpression()), symbol); + // TODO(cgdecker): There are probably other types than MethodType that we could resolve here + String parensAndMaybeEllipsis = + type instanceof MethodType && ((MethodType) type).getParameterTypes().isEmpty() + ? "()" + : "(...)"; + + String shortCallWithoutNew; + String shortCall; + if (tree.getMode() == ReferenceMode.NEW) { + shortCallWithoutNew = + state.getSourceForNode(tree.getQualifierExpression()) + parensAndMaybeEllipsis; + shortCall = "new " + shortCallWithoutNew; + } else { + shortCallWithoutNew = tree.getName() + parensAndMaybeEllipsis; + shortCall = shortCallWithoutNew; + } + + String implementedMethod = + getType(tree).asElement().getSimpleName() + + "." + + state.getTypes().findDescriptorSymbol(getType(tree).asElement()).getSimpleName(); + String methodReference = state.getSourceForNode(tree); + String message = + String.format( + "The result of `%s` must be used\n" + + "`%s` acts as an implementation of `%s`.\n" + + "— which is a `void` method, so it doesn't use the result of `%s`.\n" + + "\n" + + "To use the result, you may need to restructure your code.\n" + + "\n" + + "If you really don't want to use the result, then switch to a lambda that assigns" + + " it to a variable: `%s -> { var unused = ...; }`.\n" + + "\n" + + "If callers of `%s` shouldn't be required to use its result," + + " then annotate it with `@CanIgnoreReturnValue`.\n" + + "%s", + shortCall, + methodReference, + implementedMethod, + shortCall, + parensAndMaybeEllipsis, + shortCallWithoutNew, + apiTrailer(symbol, state)); + return buildDescription(tree).setMessage(message).build(); + } + + private String apiTrailer(MethodSymbol symbol, VisitorState state) { + if (symbol.enclClass().isAnonymous()) { + // I don't think we have a defined format for members of anonymous classes. + return ""; + } + switch (messageTrailerStyle) { + case NONE: + return ""; + case API_ERASED_SIGNATURE: + return "\n\nFull API: " + + surroundingClass(symbol) + + "#" + + methodNameAndParams(symbol, state.getTypes()); + } + throw new AssertionError(); + } + + enum MessageTrailerStyle { + NONE, + API_ERASED_SIGNATURE, } } 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 b83389beef4..54a1224df59 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 @@ -157,11 +157,11 @@ && methodParametersMatch( api.parameterTypes(), methodSymbol.params(), state.getTypes())); } - static String surroundingClass(MethodSymbol methodSymbol) { + public static String surroundingClass(MethodSymbol methodSymbol) { return methodSymbol.enclClass().getQualifiedName().toString(); } - static String methodNameAndParams(MethodSymbol methodSymbol, Types types) { + public static String methodNameAndParams(MethodSymbol methodSymbol, Types types) { return methodSymbol.name + "(" + paramsString(types, methodSymbol.params()) + ")"; } 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 388f79fc8e1..aee31f86741 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -119,7 +119,7 @@ public void testPackageAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.f();", " }", "}") @@ -140,7 +140,7 @@ public void testClassAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.f();", " }", "}") @@ -311,7 +311,7 @@ public void testNestedClassAnnotation() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " lib.Lib.Inner.InnerMost.f();", " }", "}") @@ -537,7 +537,7 @@ public void ignoreInThrowingRunnables() { " foo.f(); ", " });", " org.junit.Assert.assertThrows(IllegalStateException.class, () -> { ", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f(); ", " foo.f(); ", " });", @@ -591,13 +591,13 @@ public void onlyIgnoreWithEnclosingTryCatch() { "import static org.junit.Assert.fail;", "class Test {", " void f(Foo foo) {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " org.junit.Assert.fail();", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " junit.framework.Assert.fail();", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " foo.f();", " junit.framework.TestCase.fail();", " }", @@ -663,7 +663,7 @@ public void noCRVonClasspath() { "Test.java", "class Test {", " void m() {", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " com.google.errorprone.bugpatterns.CheckReturnValueTest.CRVTest.f();", " }", "}") @@ -680,7 +680,7 @@ public void constructor() { " @com.google.errorprone.annotations.CheckReturnValue", " public Test() {}", " public static void foo() {", - " // BUG: Diagnostic contains: Ignored return value of 'Test'", + " // BUG: Diagnostic contains: ", " new Test();", " }", "}") @@ -834,7 +834,7 @@ public void constructor_reference() { "Test.java", "class Test {", " void f() {", - " // BUG: Diagnostic contains: Ignored return value of 'Foo', which is annotated", + " // BUG: Diagnostic contains: ", " Runnable ignoresResult = Foo::new;", " }", "}") @@ -849,8 +849,7 @@ public void constructor_withoutCrvAnnotation() { "class Test {", " public Test() {}", " public static void foo() {", - " // BUG: Diagnostic contains: Ignored return value of 'Test', which wasn't" - + " annotated with @CanIgnoreReturnValue", + " // BUG: Diagnostic contains: ", " new Test();", " }", "}") @@ -865,8 +864,7 @@ public void allMethods_withoutCIRVAnnotation() { "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", + " // BUG: Diagnostic contains: ", " new Test().bar();", " }", "}") @@ -882,7 +880,7 @@ public void allMethods_withExternallyConfiguredIgnoreList() { "class Test {", " public static void foo(List x) {", " x.add(42);", - " // BUG: Diagnostic contains: Ignored return value of 'get'", + " // BUG: Diagnostic contains: ", " x.get(0);", " }", "}") @@ -906,12 +904,12 @@ public void usingElementInTestExpected() { " @Test(expected = IllegalArgumentException.class) ", " public void fooWith2Statements() {", " Foo f = new Foo();", - " // BUG: Diagnostic contains: Ignored return value of 'Foo'", + " // BUG: Diagnostic contains: ", " new Foo();", // Not OK if there is more than one statement in the block. " }", " @Test(expected = Test.None.class) ", // This is a weird way to spell the default " public void fooWithNone() {", - " // BUG: Diagnostic contains: Ignored return value of 'Foo'", + " // BUG: Diagnostic contains: ", " new Foo();", " }", "}") @@ -948,7 +946,7 @@ public void testAutoValueBuilderSetterMethods() { " static void testAnimal() {", " Animal.Builder builder = Animal.builder();", " builder.setNumberOfLegs(4);", // AutoValue.Builder setters are implicitly @CIRV - " // BUG: Diagnostic contains: Ignored return value of 'build'", + " // BUG: Diagnostic contains: ", " builder.build();", " }", "}") @@ -985,12 +983,12 @@ public void testAutoBuilderSetterMethods() { "package com.google.frobber;", "public final class PersonCaller {", " static void testPersonBuilder() {", - " // BUG: Diagnostic contains: Ignored return value of 'personBuilder'", + " // BUG: Diagnostic contains: ", " PersonBuilder.personBuilder();", " PersonBuilder builder = PersonBuilder.personBuilder();", " builder.setName(\"kurt\");", // AutoBuilder setters are implicitly @CIRV " builder.setId(42);", // AutoBuilder setters are implicitly @CIRV - " // BUG: Diagnostic contains: Ignored return value of 'build'", + " // BUG: Diagnostic contains: ", " builder.build();", " }", "}") @@ -1030,7 +1028,7 @@ public void testAutoBuilderSetterMethods_withInterface() { "import java.util.logging.Level;", "public final class LogCaller {", " static void testLogCaller() {", - " // BUG: Diagnostic contains: Ignored return value of 'logCaller'", + " // BUG: Diagnostic contains: ", " Caller.logCaller();", " Caller caller = Caller.logCaller();", " caller.setMessage(\"hi\");", // AutoBuilder setters are implicitly @CIRV @@ -1051,9 +1049,9 @@ public void testPackagesRule() { "import java.util.regex.Pattern;", "class Test {", " public static void foo(List list, Pattern pattern) {", - " // BUG: Diagnostic contains: Ignored return value of 'get'", + " // BUG: Diagnostic contains: ", " list.get(0);", - " // BUG: Diagnostic contains: Ignored return value of 'matcher'", + " // BUG: Diagnostic contains: ", " pattern.matcher(\"blah\");", " }", "}") @@ -1069,7 +1067,7 @@ public void testPackagesRule_negativePattern() { "import java.util.regex.Pattern;", "class Test {", " public static void foo(List list, Pattern pattern) {", - " // BUG: Diagnostic contains: Ignored return value of 'get'", + " // BUG: Diagnostic contains: ", " list.get(0);", " pattern.matcher(\"blah\");", " }", @@ -1090,10 +1088,10 @@ public void testPackagesRule_negativePattern_doesNotMakeOptional() { "import java.util.regex.PatternSyntaxException;", "class Test {", " public static void foo(List list, Pattern pattern) {", - " // BUG: Diagnostic contains: Ignored return value of 'get'", + " // BUG: Diagnostic contains: ", " list.get(0);", " pattern.matcher(\"blah\");", - " // BUG: Diagnostic contains: Ignored return value", + " // BUG: Diagnostic contains: ", " new PatternSyntaxException(\"\", \"\", 0);", " }", "}") diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java index e51e83c9e32..65aaab2d21c 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java @@ -31,7 +31,12 @@ private int increment(int bar) { public void foo() { int i = 1; - // BUG: Diagnostic contains: + // BUG: Diagnostic contains: The result of `increment(...)` must be used + // + // If you really don't want to use the result, then assign it to a variable: `var unused = ...`. + // + // If callers of `increment(...)` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. increment(i); System.out.println(i); } @@ -52,19 +57,46 @@ private void callRunnable(Runnable runnable) { } public void testResolvedToVoidLambda() { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: callRunnable(() -> this.intValue.increment()); } public void testResolvedToVoidMethodReference() { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: The result of `increment()` must be used + // + // `this.intValue::increment` acts as an implementation of `Runnable.run`. + // — which is a `void` method, so it doesn't use the result of `increment()`. + // + // To use the result, you may need to restructure your code. + // + // If you really don't want to use the result, then switch to a lambda that assigns it to a + // variable: `() -> { var unused = ...; }`. + // + // If callers of `increment()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. callRunnable(this.intValue::increment); } + public void testConstructorResolvedToVoidMethodReference() { + // BUG: Diagnostic contains: The result of `new MyObject()` must be used + // + // `MyObject::new` acts as an implementation of `Runnable.run`. + // — which is a `void` method, so it doesn't use the result of `new MyObject()`. + // + // To use the result, you may need to restructure your code. + // + // If you really don't want to use the result, then switch to a lambda that assigns it to a + // variable: `() -> { var unused = ...; }`. + // + // If callers of `MyObject()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. + callRunnable(MyObject::new); + } + public void testRegularLambda() { callRunnable( () -> { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: this.intValue.increment(); }); } @@ -77,7 +109,7 @@ public void testBeforeAndAfterRule() { } public void constructor() { - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: The result of `new MyObject()` must be used new MyObject() {}; class MySubObject1 extends MyObject {} @@ -92,7 +124,12 @@ class MySubObject3 extends MyObject { } } - // BUG: Diagnostic contains: Ignored return value + // BUG: Diagnostic contains: The result of `new MyObject()` must be used + // + // If you really don't want to use the result, then assign it to a variable: `var unused = ...`. + // + // If callers of `MyObject()` shouldn't be required to use its result, then annotate it with + // `@CanIgnoreReturnValue`. new MyObject(); } From b08dd0072355945e6cb6c6e5daf58c6e33e4fc4a Mon Sep 17 00:00:00 2001 From: Kevin Bierhoff Date: Thu, 21 Jul 2022 17:58:58 -0700 Subject: [PATCH 080/102] check to detect unsafe upcasts of `null` values to wildcard types. PiperOrigin-RevId: 462509760 --- .../bugpatterns/nullness/UnsafeWildcard.java | 364 ++++++++++++ .../scanner/BuiltInCheckerSuppliers.java | 2 + .../nullness/UnsafeWildcardTest.java | 558 ++++++++++++++++++ 3 files changed, 924 insertions(+) create mode 100644 core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java create mode 100644 core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java new file mode 100644 index 00000000000..08f9e93a65c --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcard.java @@ -0,0 +1,364 @@ +/* + * Copyright 2019 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.nullness; + +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static java.lang.Math.min; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.AssignmentTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ConditionalExpressionTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ParenthesizedTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.TypeCastTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.ArrayType; +import com.sun.tools.javac.code.Type.IntersectionClassType; +import com.sun.tools.javac.code.Type.MethodType; +import com.sun.tools.javac.code.Type.UnionClassType; +import com.sun.tools.javac.code.Type.WildcardType; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCExpression; +import com.sun.tools.javac.tree.JCTree.JCLambda; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; +import com.sun.tools.javac.tree.JCTree.JCNewClass; +import com.sun.tools.javac.tree.JCTree.JCParens; +import com.sun.tools.javac.tree.JCTree.JCTypeCast; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import java.util.HashSet; +import java.util.Map; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.TypeKind; + +/** Check to detect unsafe upcasts of {@code null} values to wildcard types. */ +@BugPattern(summary = "Certain wildcard types can confuse the compiler.", severity = ERROR) +public class UnsafeWildcard extends BugChecker + implements AssignmentTreeMatcher, + ClassTreeMatcher, + ConditionalExpressionTreeMatcher, + LambdaExpressionTreeMatcher, + MethodInvocationTreeMatcher, + NewClassTreeMatcher, + ParenthesizedTreeMatcher, + ReturnTreeMatcher, + TypeCastTreeMatcher, + VariableTreeMatcher { + + @Override + public Description matchAssignment(AssignmentTree tree, VisitorState state) { + return checkForUnsafeNullAssignment( + ((JCExpression) tree.getVariable()).type, tree.getExpression(), state); + } + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + JCClassDecl classDecl = (JCClassDecl) tree; + // Check "extends" and "implements" for unsafe wildcards + for (JCExpression implemented : classDecl.getImplementsClause()) { + state.reportMatch( + checkForUnsafeWildcards(implemented, "Unsafe wildcard type: ", implemented.type, state)); + } + if (classDecl.getExtendsClause() != null) { + return checkForUnsafeWildcards( + classDecl.getExtendsClause(), + "Unsafe wildcard type: ", + classDecl.getExtendsClause().type, + state); + } + return Description.NO_MATCH; + } + + @Override + public Description matchConditionalExpression( + ConditionalExpressionTree tree, VisitorState state) { + // Ternary branches are implicitly upcast, so check in case they're null + Type ternaryType = ((JCExpression) tree).type; + state.reportMatch(checkForUnsafeNullAssignment(ternaryType, tree.getTrueExpression(), state)); + return checkForUnsafeNullAssignment(ternaryType, tree.getFalseExpression(), state); + } + + @Override + public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) { + if (tree.getBody() instanceof ExpressionTree) { + Type targetType = ((JCLambda) tree).getDescriptorType(state.getTypes()).getReturnType(); + return checkForUnsafeNullAssignment(targetType, (ExpressionTree) tree.getBody(), state); + } // else covered by matchReturn + return Description.NO_MATCH; + } + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + // MethodType of this invocation gives us the args' target types with any type parameters + // substituted (unlike the method's symbol, which doesn't give us effective target types). + MethodType mtype = (MethodType) ((JCMethodInvocation) tree).meth.type; + MethodSymbol callee = ASTHelpers.getSymbol(tree); + if (!((JCMethodInvocation) tree).getTypeArguments().isEmpty()) { + // Check type arguments for problematic wildcards given in source. + for (JCExpression typearg : ((JCMethodInvocation) tree).getTypeArguments()) { + state.reportMatch( + checkForUnsafeWildcards(typearg, "Unsafe wildcard type: ", typearg.type, state)); + } + } else if (!callee.type.getTypeArguments().isEmpty()) { + // Otherwise, check any inferred type arguments + ImmutableListMultimap mapping = + ASTHelpers.getTypeSubstitution(mtype, callee); + HashSet seen = new HashSet<>(); + for (Map.Entry inferredTypearg : mapping.entries()) { + if (!seen.add(inferredTypearg.getValue())) { + continue; // avoid duplicate reports for the same type + } + state.reportMatch( + checkForUnsafeWildcards( + tree, + "Unsafe wildcard in inferred type argument for callee's type parameter " + + inferredTypearg.getKey() + + ": ", + inferredTypearg.getValue(), + state)); + } + } + + int paramIndex = 0; + for (ExpressionTree arg : tree.getArguments()) { + // Check null arguments against parameter type + // NB: this will be an array type for vararg parameters, but checkForUnsafeNullAssignment + // sees through array types, so there's no need to unwrap the type here + Type paramType = mtype.argtypes.get(paramIndex); + state.reportMatch(checkForUnsafeNullAssignment(paramType, arg, state)); + paramIndex = min(paramIndex + 1, mtype.argtypes.size() - 1); + } + return Description.NO_MATCH; + } + + @Override + public Description matchNewClass(NewClassTree tree, VisitorState state) { + // MethodType of this invocation gives us the args' target types with any type parameters + // substituted (unlike the method's symbol, which doesn't give us effective target types). + MethodType mtype = (MethodType) ((JCNewClass) tree).constructorType; + // mtype contains type of outer object in some cases, which we can skip since null enclosing + // expression would cause exception. + int paramIndex = tree.getClassBody() != null && tree.getEnclosingExpression() != null ? 1 : 0; + for (ExpressionTree arg : tree.getArguments()) { + // Check null arguments against parameter type + // NB: this will be an array type for vararg parameters, but checkForUnsafeNullAssignment + // sees through array types, so there's no need to unwrap the type here + Type paramType = mtype.argtypes.get(paramIndex); + state.reportMatch(checkForUnsafeNullAssignment(paramType, arg, state)); + paramIndex = min(paramIndex + 1, mtype.argtypes.size() - 1); + } + // Check type arguments for problematic wildcards (visiting class type will recursively visit + // its arguments + return checkForUnsafeWildcards( + tree, "Unsafe wildcard type argument: ", ((JCNewClass) tree).type, state); + } + + @Override + public Description matchParenthesized(ParenthesizedTree tree, VisitorState state) { + // Treat (null) like null + return checkForUnsafeNullAssignment(((JCParens) tree).type, tree.getExpression(), state); + } + + @Override + public Description matchReturn(ReturnTree tree, VisitorState state) { + // Check "return null" against return type + if (tree.getExpression() == null + || ((JCExpression) tree.getExpression()).type.getKind() != TypeKind.NULL) { + return Description.NO_MATCH; + } + + // Figure out return type of surrounding method or lambda and check it + Tree method = state.findEnclosing(MethodTree.class, LambdaExpressionTree.class); + if (method instanceof MethodTree) { + return checkForUnsafeNullAssignment( + ((JCMethodDecl) method).getReturnType().type, tree.getExpression(), state); + } else if (method instanceof LambdaExpressionTree) { + Type targetType = ((JCLambda) method).getDescriptorType(state.getTypes()).getReturnType(); + return checkForUnsafeNullAssignment(targetType, tree.getExpression(), state); + } + return Description.NO_MATCH; + } + + @Override + public Description matchTypeCast(TypeCastTree tree, VisitorState state) { + // Check explicit casts of null + return checkForUnsafeNullAssignment( + ((JCTypeCast) tree).getType().type, tree.getExpression(), state); + } + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + // Check initializer like assignment + if (tree.getInitializer() != null) { + return checkForUnsafeNullAssignment( + ((JCVariableDecl) tree).type, tree.getInitializer(), state); + } + + VarSymbol symbol = ASTHelpers.getSymbol(tree); + if (symbol.getKind() == ElementKind.FIELD && (symbol.flags() & Flags.FINAL) == 0) { + // Fields start out as null, so check their type as if this was a null assignment. While all + // fields start out null, we ignore them here if they're final or we checked their + // initializer above. In either case we know we'll see at least one assignment to the field, + // and if those check out they guarantees us that the field's type is ok: non-null + // assignments guarantee that there is a concrete type compatible with any + // wildcards in this field's type, and null assignments will be checked separately (possibly + // right above). + return checkForUnsafeWildcards( + tree, + "Uninitialized field with unsafe wildcard type: ", + ((JCVariableDecl) tree).type, + state); + } + return Description.NO_MATCH; + } + + /** + * Checks for unsafe wildcards in {@code targetType} if the given expression is `null`. + * + * @return diagnostic for {@code tree} if unsafe wildcard is found, {@link Description#NO_MATCH} + * otherwise. + * @see #checkForUnsafeWildcards + */ + private Description checkForUnsafeNullAssignment( + Type targetType, ExpressionTree tree, VisitorState state) { + if (!targetType.isReference() || ((JCExpression) tree).type.getKind() != TypeKind.NULL) { + return Description.NO_MATCH; + } + return checkForUnsafeWildcards(tree, "Cast to wildcard type unsafe: ", targetType, state); + } + + /** + * Recursively looks through {@code targetType} for any wildcard whose lower bounds isn't known to + * be a subtype of the corresponding type parameter's upper bound. + * + * @return diagnostic for {@code tree} with given {@code messageHeader} if unsafe wildcard found, + * {@link Description#NO_MATCH} otherwise. + */ + private Description checkForUnsafeWildcards( + Tree tree, String messageHeader, Type targetType, VisitorState state) { + while (targetType instanceof ArrayType) { + // Check array component type + targetType = ((ArrayType) targetType).getComponentType(); + } + int i = 0; + for (Type arg : targetType.getTypeArguments()) { + // Check components of generic types (getTypeArguments() is empty for other kinds of types) + if (arg instanceof WildcardType && ((WildcardType) arg).getSuperBound() != null) { + Type lowerBound = ((WildcardType) arg).getSuperBound(); + // We only check lower bounds that are themselves type variables with trivial upper bounds. + // Javac already checks other lower bounds, namely lower bounds that are concrete types or + // type variables with non-trivial upper bounds, to be in bounds of the corresponding type + // parameter (boundVar below). + // We skip these cases because the subtype check below can spuriously fail for them because + // it doesn't correctly substitute type variables when comparing lowerBound and boundVar's + // upper bound. + // Note javax.lang.model.type.TypeVariable#getUpperBound() guarantees the result to be non- + // null for type variables, so we use null check as a proxy for whether lowerBound is a type + // variable. + // TODO(kmb): avoid counting on compiler's handling of non-trivial upper bounds here + if (lowerBound.getUpperBound() != null + && lowerBound.getUpperBound().toString().endsWith("java.lang.Object")) { + Type boundVar = targetType.tsym.type.getTypeArguments().get(i); + + if (!state.getTypes().isSubtypeNoCapture(lowerBound, boundVar.getUpperBound())) { + return buildDescription(tree) + .setMessage( + messageHeader + + targetType + + " because of type argument " + + i + + " with implicit upper bound " + + boundVar.getUpperBound()) + .build(); + } + } + // Also check the super bound itself + Description contained = + checkForUnsafeWildcards(tree, messageHeader + i + " nested: ", lowerBound, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } else if (arg instanceof WildcardType && ((WildcardType) arg).getExtendsBound() != null) { + // Check the wildcard's bound + Description contained = + checkForUnsafeWildcards( + tree, + messageHeader + i + " nested: ", + ((WildcardType) arg).getExtendsBound(), + state); + if (contained != Description.NO_MATCH) { + return contained; + } + } else { + // Check for wildcards in the type argument + Description contained = + checkForUnsafeWildcards(tree, messageHeader + i + " nested: ", arg, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + ++i; + } + // For union and intersection types, check their components. + if (targetType instanceof IntersectionClassType) { + for (Type bound : ((IntersectionClassType) targetType).getExplicitComponents()) { + Description contained = + checkForUnsafeWildcards(tree, messageHeader + "bound ", bound, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + } + if (targetType instanceof UnionClassType) { + for (Type alternative : ((UnionClassType) targetType).getAlternativeTypes()) { + Description contained = + checkForUnsafeWildcards(tree, messageHeader + "alternative ", alternative, state); + if (contained != Description.NO_MATCH) { + return contained; + } + } + } + return Description.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 f45f1a6101c..d5bb5705d85 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -504,6 +504,7 @@ import com.google.errorprone.bugpatterns.nullness.ParameterMissingNullable; import com.google.errorprone.bugpatterns.nullness.ReturnMissingNullable; import com.google.errorprone.bugpatterns.nullness.UnnecessaryCheckNotNull; +import com.google.errorprone.bugpatterns.nullness.UnsafeWildcard; import com.google.errorprone.bugpatterns.nullness.VoidMissingNullable; import com.google.errorprone.bugpatterns.overloading.InconsistentOverloads; import com.google.errorprone.bugpatterns.threadsafety.DoubleCheckedLocking; @@ -1133,6 +1134,7 @@ public static ScannerSupplier errorChecks() { UnnecessarySetDefault.class, UnnecessaryStaticImport.class, UnsafeLocaleUsage.class, + UnsafeWildcard.class, UnusedException.class, UrlInSee.class, UseEnumSwitch.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java new file mode 100644 index 00000000000..71c53bfdd16 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnsafeWildcardTest.java @@ -0,0 +1,558 @@ +/* + * 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.nullness; + +import com.google.errorprone.CompilationTestHelper; +import com.sun.tools.javac.main.Main.Result; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class UnsafeWildcardTest { + + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(UnsafeWildcard.class, getClass()); + + @Test + public void positiveExpressions() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " public WithBound basic() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public WithBound inParens() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (null);", + " }", + " public WithBound cast() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (WithBound) null;", + " }", + " public WithBound inTernary(boolean x, WithBound dflt) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? null : dflt;", + " }", + " public WithBound allNullTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? null : null;", + " }", + " public WithBound parensInTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return x ? (null) : null;", + " }", + " public WithBound parensAroundTernary(boolean x) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return (x ? null : null);", + " }", + " public List> nestedWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public List> extendsWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + " public List> superWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void negativeReturns() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " public String basic() {", + " return null;", + " }", + " public String inParens() {", + " return (null);", + " }", + " public String inTernary(boolean x) {", + " return x ? null : \"foo\";", + " }", + " public String allNullTernary(boolean x) {", + " return x ? null : null;", + " }", + " public String parensInTernary(boolean x) {", + " return x ? (null) : \"foo\";", + " }", + " public String parensAroundTernary(boolean x) {", + " return (x ? null : \"foo\");", + " }", + " public List typearg() {", + " return null;", + " }", + " public List extendsWildcard() {", + " return null;", + " }", + " public List superWildcardNoImplicitBound() {", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void negativeLambdas() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.function.Function;", + "class Test {", + " public Function basic() {", + " return x -> null;", + " }", + " public Function inParens() {", + " return x -> (null);", + " }", + " public Function inTernary() {", + " return x -> x ? null : \"foo\";", + " }", + " public Function returnInLambda() {", + " return x -> { return null; };", + " }", + "}") + .doTest(); + } + + @Test + public void lambdasWithTypeParameters() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "import java.util.function.Function;", + "class Test {", + " class WithBound {}", + " public Function> contra() {", + " return s -> null;", + " }", + " public Function> implicitOk() {", + " return i -> null;", + " }", + " public Function> implicitPositive() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return u -> null;", + " }", + " public Function> returnInLambda() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return u -> { return null; };", + " }", + " public Function> nestedWildcard() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void typeParameters() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " class WildcardBound> {", + " T bad() {", + " // We allow this and instead check instantiations below", + " return null;", + " }", + " }", + " WildcardBound> diamond() {", + " // BUG: Diagnostic contains: Unsafe wildcard type argument", + " return new WildcardBound<>();", + " }", + " WildcardBound> create() {", + " // BUG: Diagnostic contains: Unsafe wildcard type argument", + " return new WildcardBound>();", + " }", + " WildcardBound> none() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return null;", + " }", + "}") + .doTest(); + } + + @Test + public void variables() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " private String s;", + " private List xs = null;", + " private List ys;", + " private WithBound zs = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " private WithBound initialized = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " private final WithBound initializedFinal = null;", + " // BUG: Diagnostic contains: Uninitialized field with unsafe wildcard", + " private WithBound uninitialized;", + " private final WithBound uninitializedFinal;", + " Test() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedFinal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitialized = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " initialized = null;", + " }", + " public void foo() {", + " List covariant = null;", + " List contravariant = null;", + " WithBound inBounds = null;", + " WithBound uninitializedLocal;", + " final WithBound uninitializedFinalLocal;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound implicitBounds = null;", + " covariant = null;", + " contravariant = null;", + " inBounds = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedLocal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " uninitializedFinalLocal = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " implicitBounds = null;", + " }", + "}") + .doTest(); + } + + @Test + public void calls() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " public void foo(String s, List xs, List contra) {", + " foo(null, null, null);", + " }", + " public void negative(WithBound xs, WithBound contra) {", + " negative(null, null);", + " }", + " public void positive(WithBound implicit) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this.positive(null);", + " positive(null);", // ok b/c compiler uses U = Object and ? super Object is ok + " }", + "}") + .doTest(); + } + + @Test + public void inferredParamType_flaggedIfProblematic() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.util.List;", + "class Test {", + " static class WithBound {}", + " public List> positive(WithBound safe) {", + " // BUG: Diagnostic contains: Unsafe wildcard in inferred type argument", + " return List.of(safe,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", // implicitly upcast to WithBound + " }", + " public List> negative(WithBound safe) {", + " return List.of(safe, null);", + " }", + "}") + .doTest(); + } + + @Test + public void constructors() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " public Test() { this(null, null); }", + " public Test(WithBound implicit) {}", + " public Test(WithBound xs, WithBound contra) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this(null);", + " }", + " class Sub extends Test {", + " Sub(WithBound implicit) {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " super(null);", + " }", + " }", + " static Test newClass() {", + " new Test(null, null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Test(null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " return new Test<>(null);", + " }", + " static Test anonymous() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Test(null) {};", + " return null;", + " }", + " void inner() {", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Sub(null);", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " new Sub(null) {};", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " this.new Sub(null) {};", + " }", + "}") + .doTest(); + } + + @Test + public void supertypes_problematicWildcards_flagged() { + compilationHelper + .addSourceLines( + "Test.java", + "import java.io.Serializable;", + "import java.util.AbstractList;", + "import java.util.List;", + "class Test {", + " class WithBound {}", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " abstract class BadList extends AbstractList> {}", + " abstract class BadListImpl implements Serializable,", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " List> {}", + " interface BadListItf extends Serializable,", + " // BUG: Diagnostic contains: Unsafe wildcard type", + " List> {}", + "}") + .doTest(); + } + + @Test + public void varargs() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " static class WithBound {}", + " Test(WithBound xs, WithBound... args) {}", + " static void hasVararg(WithBound xs, WithBound... args) {}", + " static void nullVarargs(WithBound xs) {", + " Test.hasVararg(", + " null,", // fine: target type is safe + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null,", + " xs,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", + " new Test(", + " null,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null,", + " xs,", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " null);", + " }", + "}") + .doTest(); + } + + @Test + public void arrays() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + // Generic array creation is a compilation error, and non-generic arrays are ok + " Object[] simpleInitializer = { null };", + " Object[][] nestedInitializer = { { null }, { null } };", + " void nulls() {", + " String[][] stringMatrix = null;", + " WithBound[] implicitBound = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound[] simpleNull = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " WithBound[][] nestedNull = null;", + " }", + "}") + .doTest(); + } + + /** + * Regresion test demonstrating that generic array creation is a compiler error. If it wasn't, + * we'd want to check element types. + */ + @Test + public void genericArrays_isCompilerError() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " // BUG: Diagnostic matches: X", + " WithBound[] simpleInitializer = { null };", + " // BUG: Diagnostic matches: X", + " WithBound[][] nestedInitializer = { { null }, { null } };", + " // BUG: Diagnostic matches: X", + " WithBound[][] emptyInitializer = {};", + " void newArrays() {", + " // BUG: Diagnostic matches: X", + " Object[] a1 = new WithBound[] {};", + " // BUG: Diagnostic matches: X", + " Object[] a2 = new WithBound[0];", + " // BUG: Diagnostic matches: X", + " Object[] a3 = new WithBound[][] {};", + " // BUG: Diagnostic matches: X", + " Object[] a4 = new WithBound[0][];", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage("X", msg -> msg.contains("generic array creation")) + .doTest(); + } + + @Test + public void arrays_rawTypes_futureWork() { + compilationHelper + .addSourceLines( + "Test.java", + "class Test {", + " class WithBound {}", + " void problematic() {", + // The following implicitly create problematic types even absent null values (though + // problematic non-empty arrays containing all-null values can be created just as + // easily with [N] where N > 0). The compiler issues raw and unchecked warnings here, + // but we might want to flag assignments as well. + " WithBound raw = new WithBound();", + " WithBound[] array = new WithBound[0];", + " WithBound[][] nested = new WithBound[0][];", + " }", + "}") + .doTest(); + } + + /** + * Regression test to ignore {@code null} assignment to wildcard whose lower bound is a type + * variable with non-trivial upper bound. The compiler rejects potentially dangerous wildcards on + * its own in this case, but simple subtype checks between lower and upper bound can fail and lead + * to false positives if involved type variables' upper bounds capture another type variable. + */ + @Test + public void boundedTypeVar_validLowerBound_isIgnored() { + compilationHelper + .addSourceLines( + "MyIterable.java", + "import java.util.List;", + "interface MyIterable> {", + " static class Test> implements MyIterable {", + " MyIterable parent;", + " public Test() {", + " this.parent = null;", + " }", + " }", + "}") + .doTest(); + } + + @Test + public void boundedTypeVar_questionableLowerBound_isCompilerError() { + compilationHelper + .addSourceLines( + "MyIterable.java", + "import java.util.List;", + "interface MyIterable> {", + " // BUG: Diagnostic matches: X", + " static class Test> implements MyIterable {", + " MyIterable parent;", + " public Test() {", + " this.parent = null;", + " }", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage( + "X", msg -> msg.contains("type argument S is not within bounds of type-variable T")) + .doTest(); + } + + /** + * Regression test to ignore {@code null} assignment to wildcard whose lower bound is a concrete + * type and whose implicit upper bound is F-bounded. The compiler rejects potentially dangerous + * wildcards on its own in this case, but simple subtype checks between lower and upper bound can + * fail and lead to false positives. + */ + @Test + public void fBoundedImplicitUpperBound_validLowerBound_isIgnored() { + compilationHelper + .addSourceLines( + "FBounded.java", + "abstract class FBounded> {", + " public static final class Coll extends FBounded> {}", + " public interface Listener> {}", + " public static void shouldWork() {", + " Listener> validListener = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " Listener invalidListener = null;", + " // BUG: Diagnostic contains: Cast to wildcard type unsafe", + " Iterable> invalidListeners = java.util.List.of(null, null);", + " }", + "}") + .doTest(); + } + + @Test + public void fBoundedImplicitUpperBound_invalidLowerBound_isCompilerError() { + compilationHelper + .addSourceLines( + "FBounded.java", + "abstract class FBounded> {", + " public static final class Coll extends FBounded> {}", + " public interface Listener> {}", + " public static void shouldWork() {", + " // BUG: Diagnostic matches: X", + " Listener listener = null;", + " }", + "}") + .expectResult(Result.ERROR) + .matchAllDiagnostics() + .expectErrorMessage( + "X", msg -> msg.contains("String is not within bounds of type-variable U")) + .doTest(); + } +} From a032bb4ad6894888d2686136275b6119ace42d96 Mon Sep 17 00:00:00 2001 From: ghm Date: Fri, 22 Jul 2022 03:20:21 -0700 Subject: [PATCH 081/102] Run CanIgnoreReturnValueSuggester over EP. PiperOrigin-RevId: 462583101 --- .../google/errorprone/apply/ImportOrganizer.java | 2 ++ .../errorprone/dataflow/AccessPathStore.java | 2 ++ .../NullnessPropagationTransfer.java | 3 +++ .../errorprone/fixes/BranchedSuggestedFixes.java | 4 ++++ .../google/errorprone/fixes/Replacements.java | 3 +++ .../google/errorprone/fixes/SuggestedFix.java | 16 ++++++++++++++++ .../google/errorprone/matchers/Description.java | 6 ++++++ .../com/google/errorprone/util/Commented.java | 3 +++ .../errorprone/apply/ImportStatementsTest.java | 4 ++++ .../errorprone/bugpatterns/StronglyType.java | 2 ++ .../ArgumentChangeFinder.java | 2 ++ .../bugpatterns/threadsafety/ThreadSafety.java | 10 ++++++++++ .../threadsafety/WellKnownMutability.java | 5 +++++ .../time/InvalidJavaTimeConstant.java | 3 +++ .../com/google/errorprone/refaster/Bindings.java | 2 ++ .../com/google/errorprone/refaster/Choice.java | 2 ++ .../errorprone/ErrorProneTestCompiler.java | 4 ++++ .../apidiff/CompilationBuilderHelpers.java | 5 +++++ .../nullness/UnnecessaryCheckNotNullTest.java | 2 ++ .../BugCheckerRefactoringTestHelper.java | 7 +++++++ .../google/errorprone/CompilationTestHelper.java | 11 +++++++++++ 21 files changed, 98 insertions(+) diff --git a/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java b/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java index 509398d16ac..f63e1436a7c 100644 --- a/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java +++ b/check_api/src/main/java/com/google/errorprone/apply/ImportOrganizer.java @@ -16,6 +16,7 @@ package com.google.errorprone.apply; import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Collection; import java.util.List; import java.util.Map; @@ -182,6 +183,7 @@ public String asImportBlock() { * @param keys the keys to add, in order, if a key is not in the groups then it is ignored. * @return this for chaining. */ + @CanIgnoreReturnValue public OrganizedImports addGroups( Map> groups, Iterable keys) { for (K key : keys) { diff --git a/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java b/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java index 8d9d14f4194..a17548bc8fa 100644 --- a/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java +++ b/check_api/src/main/java/com/google/errorprone/dataflow/AccessPathStore.java @@ -21,6 +21,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; @@ -114,6 +115,7 @@ public static final class Builder> { this.heap = new LinkedHashMap<>(prototype.heap()); } + @CanIgnoreReturnValue public Builder setInformation(AccessPath aPath, V value) { heap.put(checkNotNull(aPath), checkNotNull(value)); return this; 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 46509a70f54..3f3f0c6c861 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 @@ -38,6 +38,7 @@ import com.google.common.io.Files; import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.dataflow.AccessPath; import com.google.errorprone.dataflow.AccessPathStore; import com.google.errorprone.dataflow.AccessPathValues; @@ -327,6 +328,7 @@ protected NullnessPropagationTransfer( * Stores the given Javac context to find and analyze field initializers. Set before analyzing a * method and reset after. */ + @CanIgnoreReturnValue NullnessPropagationTransfer setContext(@Nullable Context context) { // This is a best-effort check (similar to ArrayList iterators, for instance), no guarantee Preconditions.checkArgument( @@ -345,6 +347,7 @@ NullnessPropagationTransfer setContext(@Nullable Context context) { * unit. Analyzing initializers from other compilation units tends to fail because type * information is sometimes missing on nodes returned from {@link Trees}. */ + @CanIgnoreReturnValue NullnessPropagationTransfer setCompilationUnit(@Nullable CompilationUnitTree compilationUnit) { this.compilationUnit = compilationUnit; return this; diff --git a/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java b/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java index b6c26e454d5..850bb2d1de5 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/BranchedSuggestedFixes.java @@ -16,6 +16,7 @@ package com.google.errorprone.fixes; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; /** * Helper class for accumulating a branching tree of alternative fixes designed to help build as set @@ -64,12 +65,14 @@ public static class Builder { private ImmutableList.Builder builder = ImmutableList.builder(); private ImmutableList savedList = ImmutableList.of(); + @CanIgnoreReturnValue public Builder startWith(SuggestedFix fix) { savedList = ImmutableList.of(); builder = ImmutableList.builder().add(fix); return this; } + @CanIgnoreReturnValue public Builder addOption(SuggestedFix fix) { if (!savedList.isEmpty()) { for (SuggestedFix s : savedList) { @@ -79,6 +82,7 @@ public Builder addOption(SuggestedFix fix) { return this; } + @CanIgnoreReturnValue public Builder then() { savedList = builder.build(); builder = ImmutableList.builder(); diff --git a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java index a7af506ea9f..615495c1bce 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/Replacements.java @@ -25,6 +25,7 @@ import com.google.common.collect.Range; import com.google.common.collect.RangeMap; import com.google.common.collect.TreeRangeMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashSet; @@ -89,10 +90,12 @@ public String coalesce(String replacement, String existing) { public abstract String coalesce(String replacement, String existing); } + @CanIgnoreReturnValue public Replacements add(Replacement replacement) { return add(replacement, CoalescePolicy.REJECT); } + @CanIgnoreReturnValue public Replacements add(Replacement replacement, CoalescePolicy coalescePolicy) { if (replacements.containsKey(replacement.range())) { Replacement existing = replacements.get(replacement.range()); diff --git a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java index e51a1dc4f41..9dec16437aa 100644 --- a/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java +++ b/check_api/src/main/java/com/google/errorprone/fixes/SuggestedFix.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -182,6 +183,7 @@ public SuggestedFix build() { return new SuggestedFix(this); } + @CanIgnoreReturnValue private Builder with(FixOperation fix) { fixes.add(fix); return this; @@ -193,11 +195,13 @@ private Builder with(FixOperation fix) { * *

Should be limited to one sentence. */ + @CanIgnoreReturnValue public Builder setShortDescription(String shortDescription) { this.shortDescription = shortDescription; return this; } + @CanIgnoreReturnValue public Builder replace(Tree node, String replaceWith) { checkNotSyntheticConstructor(node); return with(new ReplacementFix((DiagnosticPosition) node, replaceWith)); @@ -211,6 +215,7 @@ public Builder replace(Tree node, String replaceWith) { * @param endPos The position at which to end replacing, exclusive * @param replaceWith The string to replace with */ + @CanIgnoreReturnValue public Builder replace(int startPos, int endPos, String replaceWith) { DiagnosticPosition pos = new IndexedPosition(startPos, endPos); return with(new ReplacementFix(pos, replaceWith)); @@ -230,6 +235,7 @@ public Builder replace(int startPos, int endPos, String replaceWith) { * @param startPosAdjustment The adjustment to add to the start position (negative is OK) * @param endPosAdjustment The adjustment to add to the end position (negative is OK) */ + @CanIgnoreReturnValue public Builder replace( Tree node, String replaceWith, int startPosAdjustment, int endPosAdjustment) { checkNotSyntheticConstructor(node); @@ -239,21 +245,25 @@ public Builder replace( replaceWith)); } + @CanIgnoreReturnValue public Builder prefixWith(Tree node, String prefix) { checkNotSyntheticConstructor(node); return with(new PrefixInsertion((DiagnosticPosition) node, prefix)); } + @CanIgnoreReturnValue public Builder postfixWith(Tree node, String postfix) { checkNotSyntheticConstructor(node); return with(new PostfixInsertion((DiagnosticPosition) node, postfix)); } + @CanIgnoreReturnValue public Builder delete(Tree node) { checkNotSyntheticConstructor(node); return replace(node, ""); } + @CanIgnoreReturnValue public Builder swap(Tree node1, Tree node2) { checkNotSyntheticConstructor(node1); checkNotSyntheticConstructor(node2); @@ -268,6 +278,7 @@ public Builder swap(Tree node1, Tree node2) { * Add an import statement as part of this SuggestedFix. Import string should be of the form * "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder addImport(String importString) { importsToAdd.add("import " + importString); return this; @@ -277,6 +288,7 @@ public Builder addImport(String importString) { * Add a static import statement as part of this SuggestedFix. Import string should be of the * form "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder addStaticImport(String importString) { importsToAdd.add("import static " + importString); return this; @@ -286,6 +298,7 @@ public Builder addStaticImport(String importString) { * Remove an import statement as part of this SuggestedFix. Import string should be of the form * "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder removeImport(String importString) { importsToRemove.add("import " + importString); return this; @@ -295,6 +308,7 @@ public Builder removeImport(String importString) { * Remove a static import statement as part of this SuggestedFix. Import string should be of the * form "foo.bar.baz". */ + @CanIgnoreReturnValue public Builder removeStaticImport(String importString) { importsToRemove.add("import static " + importString); return this; @@ -303,6 +317,7 @@ public Builder removeStaticImport(String importString) { /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ + @CanIgnoreReturnValue public Builder merge(@Nullable Builder other) { if (other == null) { return this; @@ -319,6 +334,7 @@ public Builder merge(@Nullable Builder other) { /** * Merges all edits from {@code other} into {@code this}. If {@code other} is null, do nothing. */ + @CanIgnoreReturnValue public Builder merge(@Nullable SuggestedFix other) { if (other == null) { return this; diff --git a/check_api/src/main/java/com/google/errorprone/matchers/Description.java b/check_api/src/main/java/com/google/errorprone/matchers/Description.java index 8ca94524dd3..0df00249505 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/Description.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/Description.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.fixes.Fix; import com.google.errorprone.fixes.SuggestedFix; @@ -174,6 +175,7 @@ private Builder( * @param fix a suggested fix for this problem * @throws NullPointerException if {@code fix} is {@code null} */ + @CanIgnoreReturnValue public Builder addFix(Fix fix) { checkNotNull(fix, "fix must not be null"); if (!fix.isEmpty()) { @@ -190,6 +192,7 @@ public Builder addFix(Fix fix) { * @throws NullPointerException if {@code fix} is {@code null} * @deprecated prefer referring to empty fixes using {@link SuggestedFix#emptyFix()}. */ + @CanIgnoreReturnValue @Deprecated public Builder addFix(Optional fix) { checkNotNull(fix, "fix must not be null"); @@ -203,6 +206,7 @@ public Builder addFix(Optional fix) { * @param fixes a list of suggested fixes for this problem * @throws NullPointerException if {@code fixes} or any of its elements are {@code null} */ + @CanIgnoreReturnValue public Builder addAllFixes(List fixes) { checkNotNull(fixes, "fixes must not be null"); for (Fix fix : fixes) { @@ -217,6 +221,7 @@ public Builder addAllFixes(List fixes) { * * @param message A custom error message without the check name ("[checkname]") or link */ + @CanIgnoreReturnValue public Builder setMessage(String message) { checkNotNull(message, "message must not be null"); this.rawMessage = message; @@ -227,6 +232,7 @@ public Builder setMessage(String message) { * Set a custom link URL. The custom URL will be used instead of the default one which forms * part of the {@code @}BugPattern. */ + @CanIgnoreReturnValue public Builder setLinkUrl(String linkUrl) { checkNotNull(linkUrl, "linkUrl must not be null"); this.linkUrl = linkUrl; diff --git a/check_api/src/main/java/com/google/errorprone/util/Commented.java b/check_api/src/main/java/com/google/errorprone/util/Commented.java index 529f5a97c80..3b1282142cd 100644 --- a/check_api/src/main/java/com/google/errorprone/util/Commented.java +++ b/check_api/src/main/java/com/google/errorprone/util/Commented.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.source.tree.Tree; import com.sun.tools.javac.parser.Tokens.Comment; @@ -51,6 +52,7 @@ abstract static class Builder { protected abstract ImmutableList.Builder afterCommentsBuilder(); + @CanIgnoreReturnValue Builder addComment( Comment comment, int nodePosition, int tokenizingOffset, Position position) { OffsetComment offsetComment = new OffsetComment(comment, tokenizingOffset); @@ -67,6 +69,7 @@ Builder addComment( return this; } + @CanIgnoreReturnValue Builder addAllComment( Iterable comments, int nodePosition, diff --git a/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java b/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java index 559a874e3be..b1a28b9a528 100644 --- a/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java +++ b/check_api/src/test/java/com/google/errorprone/apply/ImportStatementsTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.source.tree.TreeVisitor; import com.sun.tools.javac.tree.EndPosTable; import com.sun.tools.javac.tree.JCTree; @@ -97,6 +98,7 @@ private static class StubImportBuilder { * @param typeName the fully-qualified name of the type being imported * @return a new JCImport stub */ + @CanIgnoreReturnValue StubImportBuilder addImport(String typeName) { return addImport(typeName, /* isStatic= */ false); } @@ -107,6 +109,7 @@ StubImportBuilder addImport(String typeName) { * @param typeName the fully-qualified name of the type being imported * @return a new JCImport stub */ + @CanIgnoreReturnValue StubImportBuilder addStaticImport(String typeName) { return addImport(typeName, /* isStatic= */ true); } @@ -118,6 +121,7 @@ StubImportBuilder addStaticImport(String typeName) { * @param isStatic whether the import is static * @return a new JCImport stub */ + @CanIgnoreReturnValue private StubImportBuilder addImport(String typeName, boolean isStatic) { // craft import string StringBuilder returnSB = new StringBuilder("import "); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java b/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java index ec9e6063a27..03d38150575 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/StronglyType.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; @@ -94,6 +95,7 @@ public abstract static class Builder { abstract ImmutableSet.Builder primitiveTypesToReplaceBuilder(); /** Add a type that can be replaced with a stronger type. */ + @CanIgnoreReturnValue public final Builder addType(Type type) { primitiveTypesToReplaceBuilder().add(type); return this; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java index 4a38cb00f26..8b6bf999aa0 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/argumentselectiondefects/ArgumentChangeFinder.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.function.Function; /** @@ -58,6 +59,7 @@ abstract static class Builder { * eliminating spurious findings. Heuristics are applied in order so add more expensive checks * last. */ + @CanIgnoreReturnValue Builder addHeuristic(Heuristic heuristic) { heuristicsBuilder().add(heuristic); return this; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java index 8665c5ac6bf..4519338204a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafety.java @@ -29,6 +29,7 @@ import com.google.common.collect.Sets; import com.google.common.collect.Streams; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.CanBeStaticAnalyzer; import com.google.errorprone.suppliers.Supplier; import com.google.errorprone.util.ASTHelpers; @@ -145,12 +146,14 @@ private Builder() {} @Nullable private Class typeParameterAnnotation; /** See {@link Purpose}. */ + @CanIgnoreReturnValue public Builder setPurpose(Purpose purpose) { this.purpose = purpose; return this; } /** Information about known types and whether they're known to be safe or unsafe. */ + @CanIgnoreReturnValue public Builder knownTypes(KnownTypes knownTypes) { this.knownTypes = knownTypes; return this; @@ -160,11 +163,13 @@ public Builder knownTypes(KnownTypes knownTypes) { * Annotations that will cause a class to be tested with this {@link ThreadSafety} instance; for * example, when testing a class for immutability, this should be @Immutable. */ + @CanIgnoreReturnValue public Builder markerAnnotations(Set markerAnnotations) { return markerAnnotations(ImmutableSet.copyOf(markerAnnotations)); } // TODO(ringwalt): Remove this constructor. We need it for binary compatibility. + @CanIgnoreReturnValue public Builder markerAnnotations(ImmutableSet markerAnnotations) { checkNotNull(markerAnnotations); this.markerAnnotations = markerAnnotations; @@ -177,11 +182,13 @@ public Builder markerAnnotations(ImmutableSet markerAnnotations) { * annotation, @Immutable would be included in this list, as an immutable class is by definition * thread-safe. */ + @CanIgnoreReturnValue public Builder acceptedAnnotations(Set acceptedAnnotations) { return acceptedAnnotations(ImmutableSet.copyOf(acceptedAnnotations)); } // TODO(ringwalt): Remove this constructor. We need it for binary compatibility. + @CanIgnoreReturnValue public Builder acceptedAnnotations(ImmutableSet acceptedAnnotations) { checkNotNull(acceptedAnnotations); this.acceptedAnnotations = acceptedAnnotations; @@ -189,6 +196,7 @@ public Builder acceptedAnnotations(ImmutableSet acceptedAnnotations) { } /** An annotation which marks a generic parameter as a container type. */ + @CanIgnoreReturnValue public Builder containerOfAnnotation(Class containerOfAnnotation) { checkNotNull(containerOfAnnotation); this.containerOfAnnotation = containerOfAnnotation; @@ -196,6 +204,7 @@ public Builder containerOfAnnotation(Class containerOfAnno } /** An annotation which, when found on a class, should suppress the test */ + @CanIgnoreReturnValue public Builder suppressAnnotation(Class suppressAnnotation) { checkNotNull(suppressAnnotation); this.suppressAnnotation = suppressAnnotation; @@ -206,6 +215,7 @@ public Builder suppressAnnotation(Class suppressAnnotation * An annotation which, when found on a type parameter, indicates that the type parameter may * only be instantiated with thread-safe types. */ + @CanIgnoreReturnValue public Builder typeParameterAnnotation(Class typeParameterAnnotation) { checkNotNull(typeParameterAnnotation); checkArgument( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java index cc947940433..42352e603ae 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/WellKnownMutability.java @@ -27,6 +27,7 @@ import com.google.common.primitives.Primitives; import com.google.errorprone.ErrorProneFlags; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.bugpatterns.ImmutableCollections; import com.google.errorprone.suppliers.Supplier; @@ -96,6 +97,7 @@ public Set getKnownUnsafeClasses() { static class Builder { final ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + @CanIgnoreReturnValue public Builder addClasses(Set> clazzs) { for (Class clazz : clazzs) { add(clazz); @@ -103,6 +105,7 @@ public Builder addClasses(Set> clazzs) { return this; } + @CanIgnoreReturnValue public Builder addStrings(List classNames) { for (String className : classNames) { add(className); @@ -110,6 +113,7 @@ public Builder addStrings(List classNames) { return this; } + @CanIgnoreReturnValue public Builder add(Class clazz, String... containerOf) { ImmutableSet containerTyParams = ImmutableSet.copyOf(containerOf); HashSet actualTyParams = new HashSet<>(); @@ -129,6 +133,7 @@ public Builder add(Class clazz, String... containerOf) { return this; } + @CanIgnoreReturnValue public Builder add(String className, String... containerOf) { mapBuilder.put( className, AnnotationInfo.create(className, ImmutableList.copyOf(containerOf))); diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java b/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java index 8bdbd284f14..72b3a551c78 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/time/InvalidJavaTimeConstant.java @@ -41,6 +41,7 @@ import com.google.common.collect.Maps; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.matchers.Description; @@ -111,6 +112,7 @@ public abstract static class Builder { abstract ImmutableList.Builder methodsBuilder(); + @CanIgnoreReturnValue public Builder addStaticMethod(String methodName, Param... params) { methodsBuilder() .add( @@ -123,6 +125,7 @@ public Builder addStaticMethod(String methodName, Param... params) { return this; } + @CanIgnoreReturnValue public Builder addInstanceMethod(String methodName, Param... params) { methodsBuilder() .add( diff --git a/core/src/main/java/com/google/errorprone/refaster/Bindings.java b/core/src/main/java/com/google/errorprone/refaster/Bindings.java index b96fdaa0528..1b4754dfdb8 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Bindings.java +++ b/core/src/main/java/com/google/errorprone/refaster/Bindings.java @@ -23,6 +23,7 @@ import com.google.common.collect.ForwardingMap; import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -128,6 +129,7 @@ public V putBinding(Key key, V value) { return (V) super.put(key, value); } + @CanIgnoreReturnValue @Nullable @Override public Object put(Key key, Object value) { diff --git a/core/src/main/java/com/google/errorprone/refaster/Choice.java b/core/src/main/java/com/google/errorprone/refaster/Choice.java index 6e1c27ec600..b385314b3d0 100644 --- a/core/src/main/java/com/google/errorprone/refaster/Choice.java +++ b/core/src/main/java/com/google/errorprone/refaster/Choice.java @@ -25,6 +25,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; import java.util.Collection; import java.util.Collections; @@ -78,6 +79,7 @@ public Choice or(Choice other) { return checkNotNull(other); } + @CanIgnoreReturnValue @Override public Choice condition(Predicate predicate) { checkNotNull(predicate); diff --git a/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java b/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java index 089d6d36b4f..ef2fb5b91f1 100644 --- a/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java +++ b/core/src/test/java/com/google/errorprone/ErrorProneTestCompiler.java @@ -16,6 +16,7 @@ package com.google.errorprone; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.scanner.ScannerSupplier; import com.sun.tools.javac.main.Main.Result; import java.io.PrintWriter; @@ -40,16 +41,19 @@ public ErrorProneTestCompiler build() { return new ErrorProneTestCompiler(listener, scannerSupplier, printWriter); } + @CanIgnoreReturnValue public Builder listenToDiagnostics(DiagnosticListener listener) { this.listener = listener; return this; } + @CanIgnoreReturnValue public Builder report(ScannerSupplier scannerSupplier) { this.scannerSupplier = scannerSupplier; return this; } + @CanIgnoreReturnValue public Builder redirectOutputTo(PrintWriter printWriter) { this.printWriter = printWriter; return this; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java b/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java index 3d06e0a99a9..ec380cb6d4b 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/apidiff/CompilationBuilderHelpers.java @@ -22,6 +22,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.sun.tools.javac.file.JavacFileManager; import java.io.File; import java.io.IOException; @@ -60,6 +61,7 @@ public SourceBuilder(File tempFolder) { this.tempFolder = tempFolder; } + @CanIgnoreReturnValue public SourceBuilder addSourceLines(String name, String... lines) throws IOException { Path filePath = Paths.get(tempFolder.getAbsolutePath(), name); sources.add(filePath); @@ -90,16 +92,19 @@ public CompilationBuilder( this.fileManager = fileManager; } + @CanIgnoreReturnValue public CompilationBuilder setSources(Collection sources) { this.sources = sources; return this; } + @CanIgnoreReturnValue public CompilationBuilder setClasspath(Collection classpath) { this.classpath = classpath; return this; } + @CanIgnoreReturnValue CompilationBuilder setJavacopts(Iterable javacopts) { this.javacopts = javacopts; return this; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java index e3ff1c667cd..2b43061b2fa 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/nullness/UnnecessaryCheckNotNullTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.Lists; import com.google.errorprone.CompilationTestHelper; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.matchers.CompilerBasedAbstractTest; import com.google.errorprone.scanner.Scanner; import com.sun.source.tree.ExpressionStatementTree; @@ -226,6 +227,7 @@ private Match(String... expected) { private static class Builder { private final ImmutableMap.Builder builder = ImmutableMap.builder(); + @CanIgnoreReturnValue public Builder add(String expression, String... expected) { builder.put(expression, new Match(expected)); return this; diff --git a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java index b18b57ffabd..09220a4fa16 100644 --- a/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java +++ b/test_helpers/src/main/java/com/google/errorprone/BugCheckerRefactoringTestHelper.java @@ -198,11 +198,13 @@ public BugCheckerRefactoringTestHelper.ExpectOutput addInputLines(String path, S return new ExpectOutput(forSourceLines(path, input)); } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setFixChooser(FixChooser chooser) { this.fixChooser = chooser; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper addModules(String... modules) { return setArgs( Arrays.stream(modules) @@ -210,23 +212,27 @@ public BugCheckerRefactoringTestHelper addModules(String... modules) { .collect(toImmutableList())); } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setArgs(ImmutableList args) { checkState(options.isEmpty()); this.options = args; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setArgs(String... args) { this.options = ImmutableList.copyOf(args); return this; } /** If set, fixes that produce output that doesn't compile are allowed. Off by default. */ + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper allowBreakingChanges() { allowBreakingChanges = true; return this; } + @CanIgnoreReturnValue public BugCheckerRefactoringTestHelper setImportOrder(String importOrder) { this.importOrder = importOrder; return this; @@ -248,6 +254,7 @@ public void doTest(TestMode testMode) { } } + @CanIgnoreReturnValue private BugCheckerRefactoringTestHelper addInputAndOutput( JavaFileObject input, JavaFileObject output) { sources.put(input, output); diff --git a/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java b/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java index 8d46b5a5460..fb639ef9aee 100644 --- a/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java +++ b/test_helpers/src/main/java/com/google/errorprone/CompilationTestHelper.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.errorprone.DiagnosticTestHelper.LookForCheckNameInDiagnostic; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.scanner.ScannerSupplier; @@ -181,6 +182,7 @@ private static Optional getOverrideClasspath(@Nullable List> over */ // TODO(eaftan): We could eliminate this path parameter and just infer the path from the // package and class name + @CanIgnoreReturnValue public CompilationTestHelper addSourceLines(String path, String... lines) { this.sources.add(forSourceLines(path, lines)); return this; @@ -193,6 +195,7 @@ public CompilationTestHelper addSourceLines(String path, String... lines) { * * @param path the path to the source file */ + @CanIgnoreReturnValue public CompilationTestHelper addSourceFile(String path) { this.sources.add(forResource(clazz, path)); return this; @@ -205,11 +208,13 @@ public CompilationTestHelper addSourceFile(String path) { * * @param classes the class(es) to use as the classpath */ + @CanIgnoreReturnValue public CompilationTestHelper withClasspath(Class... classes) { this.overrideClasspath = ImmutableList.copyOf(classes); return this; } + @CanIgnoreReturnValue public CompilationTestHelper addModules(String... modules) { return setArgs( stream(modules) @@ -221,6 +226,7 @@ public CompilationTestHelper addModules(String... modules) { * Sets custom command-line arguments for the compilation. These will be appended to the default * compilation arguments. */ + @CanIgnoreReturnValue public CompilationTestHelper setArgs(String... args) { return setArgs(asList(args)); } @@ -229,6 +235,7 @@ public CompilationTestHelper setArgs(String... args) { * Sets custom command-line arguments for the compilation. These will be appended to the default * compilation arguments. */ + @CanIgnoreReturnValue public CompilationTestHelper setArgs(List args) { checkState( extraArgs.isEmpty(), @@ -244,6 +251,7 @@ public CompilationTestHelper setArgs(List args) { * source file contains bug markers. Useful for testing that a check is actually disabled when the * proper command-line argument is passed. */ + @CanIgnoreReturnValue public CompilationTestHelper expectNoDiagnostics() { this.expectNoDiagnostics = true; return this; @@ -254,6 +262,7 @@ public CompilationTestHelper expectNoDiagnostics() { * tested. This behaviour can be disabled to test the interaction between Error Prone checks and * javac diagnostics. */ + @CanIgnoreReturnValue public CompilationTestHelper matchAllDiagnostics() { this.lookForCheckNameInDiagnostic = LookForCheckNameInDiagnostic.NO; return this; @@ -263,6 +272,7 @@ public CompilationTestHelper matchAllDiagnostics() { * Tells the compilation helper to expect a specific result from the compilation, e.g. success or * failure. */ + @CanIgnoreReturnValue public CompilationTestHelper expectResult(Result result) { expectedResult = Optional.of(result); return this; @@ -282,6 +292,7 @@ public CompilationTestHelper expectResult(Result result) { * *

Error message keys that don't match any diagnostics will cause test to fail. */ + @CanIgnoreReturnValue public CompilationTestHelper expectErrorMessage(String key, Predicate matcher) { diagnosticHelper.expectErrorMessage(key, matcher); return this; From 147031d5852f93fef41e8c101fdc8c4102a39228 Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 25 Jul 2022 03:37:02 -0700 Subject: [PATCH 082/102] Handle generated ClientInterfaces in CheckReturnValue. PiperOrigin-RevId: 463047418 --- .../bugpatterns/CheckReturnValue.java | 2 ++ .../checkreturnvalue/ProtoRules.java | 23 +++++++++++++++++++ .../bugpatterns/CheckReturnValueTest.java | 20 ++++++++++++++++ 3 files changed, 45 insertions(+) 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 051f98534b1..46f60ccda06 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -24,6 +24,7 @@ import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.methodNameAndParams; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.surroundingClass; +import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.clientInterfaces; 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; @@ -109,6 +110,7 @@ public CheckReturnValue(ErrorProneFlags flags) { mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), protoBuilders(), + clientInterfaces(), mutableProtos(), autoValues(), autoValueBuilders(), 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 8350f68c5a1..1d1fbac4b84 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 @@ -17,7 +17,9 @@ package com.google.errorprone.bugpatterns.checkreturnvalue; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.isSubtype; +import static java.util.stream.Stream.concat; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule; @@ -26,6 +28,7 @@ import com.sun.tools.javac.code.Type; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Stream; /** Rules for methods on proto messages and builders. */ public final class ProtoRules { @@ -49,6 +52,26 @@ public static ResultUseRule mutableProtos() { supplier("com.google.protobuf.AbstractMutableMessageLite"), "MUTABLE_PROTO"); } + /** Handles generated ClientInterfaces. */ + public static ResultUseRule clientInterfaces() { + return new MethodRule() { + @Override + public Optional evaluateMethod(MethodSymbol method, VisitorState state) { + // We want to exempt implementations of methods on ClientInterface too (e.g. Stub). + if (concat(Stream.of(method), findSuperMethods(method, state.getTypes()).stream()) + .anyMatch(m -> m.owner.getSimpleName().contentEquals("ClientInterface"))) { + return Optional.of(ResultUsePolicy.OPTIONAL); + } + return Optional.empty(); + } + + @Override + public String id() { + return "ClientInterface"; + } + }; + } + // TODO(cgdecker): Move proto rules from IgnoredPureGetter and ReturnValueIgnored here /** Rules for methods on protos. */ 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 aee31f86741..4a86d676501 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -871,6 +871,26 @@ public void allMethods_withoutCIRVAnnotation() { .doTest(); } + @Test + public void clientInterface() { + compilationHelperLookingAtAllMethods() + .addSourceLines( + "Test.java", + "interface ClientInterface {", + " Object foo();", + " public static final class Stub implements ClientInterface {", + " @Override public Object foo() {", + " return null;", + " }", + " }", + " default void test() {", + " foo();", + " new Stub().foo();", + " }", + "}") + .doTest(); + } + @Test public void allMethods_withExternallyConfiguredIgnoreList() { compileWithExternalApis("java.util.List#add(java.lang.Object)") From e8f0f872c992c266f4748fcd549596792127df4d Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Mon, 25 Jul 2022 08:29:08 -0700 Subject: [PATCH 083/102] Add a test demonstrating b/240039465 #checkreturnvalue PiperOrigin-RevId: 463091405 --- .../CanIgnoreReturnValueSuggesterTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java index ae23907c58a..5ae9f1e09b9 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -451,4 +451,47 @@ public void testDelegateToCirvMethod() { "}") .doTest(); } + + @Test + public void testConverter_b240039465() { + helper + .addInputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.common.base.Converter;", + "public final class Client extends Converter {", + " public Integer badMethod(String value) {", + " return convert(value);", + " }", + " @Override", + " public Integer doForward(String value) {", + " return Integer.parseInt(value);", + " }", + " @Override", + " public String doBackward(Integer value) {", + " return String.valueOf(value);", + " }", + "}") + // TODO(b/240039465): this should be .expectUnchanged() + .addOutputLines( + "Client.java", + "package com.google.frobber;", + "import com.google.common.base.Converter;", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "public final class Client extends Converter {", + " @CanIgnoreReturnValue", + " public Integer badMethod(String value) {", + " return convert(value);", + " }", + " @Override", + " public Integer doForward(String value) {", + " return Integer.parseInt(value);", + " }", + " @Override", + " public String doBackward(Integer value) {", + " return String.valueOf(value);", + " }", + "}") + .doTest(); + } } From 7d555ef157dbdf3db531bc65356fd4d92ff13357 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Mon, 25 Jul 2022 10:35:13 -0700 Subject: [PATCH 084/102] Fix issue with CanIgnoreReturnValueSuggester. When the method that produced the (ignorable) value to return: * Is a generic method * Whose upper bound was a supertype of the method we're analyzing We would incorrectly choose to believe that the value coming from the other method was substitutable for `this`. We fix this by ensuring that we resolve the generic return type, and by ensuring that the surrounding type *could* be returned by the method we're analyzing. PiperOrigin-RevId: 463119481 --- .../CanIgnoreReturnValueSuggester.java | 29 ++++++++------ .../CanIgnoreReturnValueSuggesterTest.java | 40 +++++++------------ 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java index 64688ff45ca..87f861c3594 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggester.java @@ -18,11 +18,14 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.fixes.SuggestedFixes.qualifyType; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getReturnType; 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.isVoidType; +import static com.google.errorprone.util.ASTHelpers.stripParentheses; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; @@ -31,7 +34,6 @@ import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.suppliers.Supplier; -import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LambdaExpressionTree; @@ -119,7 +121,7 @@ private static boolean isSimpleReturnThisMethod(MethodTree methodTree) { } private static boolean isIdentifier(ExpressionTree expr, String identifierName) { - expr = ASTHelpers.stripParentheses(expr); + expr = stripParentheses(expr); if (expr instanceof IdentifierTree) { return ((IdentifierTree) expr).getName().contentEquals(identifierName); } @@ -157,13 +159,15 @@ private static boolean isDefinitionOfZeroArgSelf(MethodSymbol methodSymbol) { private static boolean methodReturnsIgnorableValues(MethodTree tree, VisitorState state) { class ReturnValuesFromMethodAreIgnorable extends TreeScanner { private final VisitorState state; - private MethodSymbol symbol; + private final Type enclosingClassType; + private final Type methodReturnType; private boolean atLeastOneReturn = false; private boolean allReturnsIgnorable = true; private ReturnValuesFromMethodAreIgnorable(VisitorState state, MethodSymbol methSymbol) { this.state = state; - this.symbol = methSymbol; + this.methodReturnType = methSymbol.getReturnType(); + this.enclosingClassType = methSymbol.enclClass().type; } @Override @@ -181,16 +185,18 @@ private boolean isIgnorableMethodCallOnSameInstance( ReturnTree returnTree, VisitorState state) { if (returnTree.getExpression() instanceof MethodInvocationTree) { MethodInvocationTree mit = (MethodInvocationTree) returnTree.getExpression(); - ExpressionTree receiver = ASTHelpers.getReceiver(mit); + ExpressionTree receiver = getReceiver(mit); MethodSymbol calledMethod = getSymbol(mit); if ((receiver == null && !calledMethod.isStatic()) || isIdentifier(receiver, "this") || isIdentifier(receiver, "super")) { - // If the method we're calling is @CIRV and returns a subtype of the enclosing symbol, - // then we think it's likely ~this. - return ASTHelpers.hasAnnotation(calledMethod, CIRV, state) - && ASTHelpers.isSubtype( - symbol.enclClass().type, calledMethod.getReturnType(), state); + // If the method we're calling is @CIRV and the enclosing class could be represented by + // the object being returned by the other method, then it's probable that the other + // method is likely to + // be an ignorable result. + return hasAnnotation(calledMethod, CIRV, state) + && isSubtype(enclosingClassType, methodReturnType, state) + && isSubtype(enclosingClassType, getReturnType(mit), state); } } return false; @@ -209,8 +215,7 @@ public Void visitNewClass(NewClassTree node, Void unused) { } } - ReturnValuesFromMethodAreIgnorable scanner = - new ReturnValuesFromMethodAreIgnorable(state, ASTHelpers.getSymbol(tree)); + var scanner = new ReturnValuesFromMethodAreIgnorable(state, getSymbol(tree)); scanner.scan(tree, null); return scanner.atLeastOneReturn && scanner.allReturnsIgnorable; } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java index 5ae9f1e09b9..d9fbf166552 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/checkreturnvalue/CanIgnoreReturnValueSuggesterTest.java @@ -456,42 +456,30 @@ public void testDelegateToCirvMethod() { public void testConverter_b240039465() { helper .addInputLines( - "Client.java", + "Parent.java", "package com.google.frobber;", - "import com.google.common.base.Converter;", - "public final class Client extends Converter {", - " public Integer badMethod(String value) {", - " return convert(value);", - " }", - " @Override", - " public Integer doForward(String value) {", - " return Integer.parseInt(value);", - " }", - " @Override", - " public String doBackward(Integer value) {", - " return String.valueOf(value);", - " }", + "import com.google.errorprone.annotations.CanIgnoreReturnValue;", + "abstract class Parent {", + " @CanIgnoreReturnValue", + " X doFrom(String in) { return from(in); }", + " abstract X from(String value);", "}") - // TODO(b/240039465): this should be .expectUnchanged() - .addOutputLines( + .expectUnchanged() + .addInputLines( "Client.java", "package com.google.frobber;", - "import com.google.common.base.Converter;", - "import com.google.errorprone.annotations.CanIgnoreReturnValue;", - "public final class Client extends Converter {", - " @CanIgnoreReturnValue", + "public final class Client extends Parent {", + // While doFrom(String) is @CIRV, since it returns Integer, and not Client, we don't add + // @CIRV here. " public Integer badMethod(String value) {", - " return convert(value);", + " return doFrom(value);", " }", " @Override", - " public Integer doForward(String value) {", + " public Integer from(String value) {", " return Integer.parseInt(value);", " }", - " @Override", - " public String doBackward(Integer value) {", - " return String.valueOf(value);", - " }", "}") + .expectUnchanged() .doTest(); } } From 4c03db53805687f3779e3c219d113ffb75cb0ccd Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 26 Jul 2022 03:47:32 -0700 Subject: [PATCH 085/102] Jiggle ProtoFieldNullComparison:MatchTestAssertions around to prepare making it a build error. PiperOrigin-RevId: 463298416 --- .../google/errorprone/bugpatterns/ProtoFieldNullComparison.java | 2 +- .../errorprone/bugpatterns/ProtoFieldNullComparisonTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java index e54cacb0bc1..599bcf7ef8b 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparison.java @@ -140,7 +140,7 @@ private static boolean isNull(ExpressionTree tree) { public ProtoFieldNullComparison(ErrorProneFlags flags) { this.matchTestAssertions = - flags.getBoolean("ProtoFieldNullComparison:MatchTestAssertions").orElse(false); + flags.getBoolean("ProtoFieldNullComparison:MatchTestAssertions").orElse(true); } @Override diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java index 0794f7bea7d..db81b06f823 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ProtoFieldNullComparisonTest.java @@ -343,7 +343,6 @@ public void assertions() { " ProtoTruth.assertThat(message.getMessage()).isNotNull();", " }", "}") - .setArgs(ImmutableList.of("-XepOpt:ProtoFieldNullComparison:MatchTestAssertions")) .doTest(); } @@ -365,6 +364,7 @@ public void assertions_negative() { " assertThat(message.getMessage()).isNotNull();", " }", "}") + .setArgs(ImmutableList.of("-XepOpt:ProtoFieldNullComparison:MatchTestAssertions=false")) .doTest(); } From ab72b0301e658701fc1ca146ae4f3515ec68475e Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 26 Jul 2022 03:57:46 -0700 Subject: [PATCH 086/102] Internalise ClientInterface stuff. PiperOrigin-RevId: 463299720 --- .../bugpatterns/CheckReturnValue.java | 2 -- .../checkreturnvalue/ProtoRules.java | 23 ------------------- .../bugpatterns/CheckReturnValueTest.java | 20 ---------------- 3 files changed, 45 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 46f60ccda06..051f98534b1 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/CheckReturnValue.java @@ -24,7 +24,6 @@ import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.externalIgnoreList; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.methodNameAndParams; import static com.google.errorprone.bugpatterns.checkreturnvalue.ExternalCanIgnoreReturnValue.surroundingClass; -import static com.google.errorprone.bugpatterns.checkreturnvalue.ProtoRules.clientInterfaces; 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; @@ -110,7 +109,6 @@ public CheckReturnValue(ErrorProneFlags flags) { mapAnnotationSimpleName(CHECK_RETURN_VALUE, EXPECTED), mapAnnotationSimpleName(CAN_IGNORE_RETURN_VALUE, OPTIONAL), protoBuilders(), - clientInterfaces(), mutableProtos(), autoValues(), autoValueBuilders(), 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 1d1fbac4b84..8350f68c5a1 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 @@ -17,9 +17,7 @@ package com.google.errorprone.bugpatterns.checkreturnvalue; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.isSubtype; -import static java.util.stream.Stream.concat; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.checkreturnvalue.ResultUseRule.MethodRule; @@ -28,7 +26,6 @@ import com.sun.tools.javac.code.Type; import java.util.Optional; import java.util.regex.Pattern; -import java.util.stream.Stream; /** Rules for methods on proto messages and builders. */ public final class ProtoRules { @@ -52,26 +49,6 @@ public static ResultUseRule mutableProtos() { supplier("com.google.protobuf.AbstractMutableMessageLite"), "MUTABLE_PROTO"); } - /** Handles generated ClientInterfaces. */ - public static ResultUseRule clientInterfaces() { - return new MethodRule() { - @Override - public Optional evaluateMethod(MethodSymbol method, VisitorState state) { - // We want to exempt implementations of methods on ClientInterface too (e.g. Stub). - if (concat(Stream.of(method), findSuperMethods(method, state.getTypes()).stream()) - .anyMatch(m -> m.owner.getSimpleName().contentEquals("ClientInterface"))) { - return Optional.of(ResultUsePolicy.OPTIONAL); - } - return Optional.empty(); - } - - @Override - public String id() { - return "ClientInterface"; - } - }; - } - // TODO(cgdecker): Move proto rules from IgnoredPureGetter and ReturnValueIgnored here /** Rules for methods on protos. */ 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 4a86d676501..aee31f86741 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/CheckReturnValueTest.java @@ -871,26 +871,6 @@ public void allMethods_withoutCIRVAnnotation() { .doTest(); } - @Test - public void clientInterface() { - compilationHelperLookingAtAllMethods() - .addSourceLines( - "Test.java", - "interface ClientInterface {", - " Object foo();", - " public static final class Stub implements ClientInterface {", - " @Override public Object foo() {", - " return null;", - " }", - " }", - " default void test() {", - " foo();", - " new Stub().foo();", - " }", - "}") - .doTest(); - } - @Test public void allMethods_withExternallyConfiguredIgnoreList() { compileWithExternalApis("java.util.List#add(java.lang.Object)") From 450ab50c0c245b7b67f09910c435b91df9a49d55 Mon Sep 17 00:00:00 2001 From: ghm Date: Tue, 26 Jul 2022 07:11:15 -0700 Subject: [PATCH 087/102] Add a streamSuperMethods alongside findSuperMethods, because it's very common to immediately restream the collected stream. PiperOrigin-RevId: 463328245 --- .../errorprone/matchers/JUnitMatchers.java | 5 ++--- .../errorprone/matchers/TestNgMatchers.java | 5 ++--- .../com/google/errorprone/util/ASTHelpers.java | 9 +++++++++ .../bugpatterns/ForOverrideChecker.java | 4 ++-- .../bugpatterns/MissingOverride.java | 4 ++-- .../bugpatterns/NoAllocationChecker.java | 4 ++-- .../bugpatterns/RestrictedApiChecker.java | 18 +++++++++--------- .../bugpatterns/RxReturnValueIgnored.java | 3 ++- 8 files changed, 30 insertions(+), 22 deletions(-) diff --git a/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java b/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java index 80fc2a7b3e0..85b98bb1bac 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java @@ -37,8 +37,8 @@ import static com.google.errorprone.matchers.Matchers.nestingKind; import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.suppliers.Suppliers.VOID_TYPE; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static javax.lang.model.element.NestingKind.TOP_LEVEL; import com.google.common.collect.ImmutableList; @@ -86,8 +86,7 @@ public static boolean hasJUnitAnnotation(MethodTree tree, VisitorState state) { if (hasJUnitAttr(methodSym)) { return true; } - return findSuperMethods(methodSym, state.getTypes()).stream() - .anyMatch(JUnitMatchers::hasJUnitAttr); + return streamSuperMethods(methodSym, state.getTypes()).anyMatch(JUnitMatchers::hasJUnitAttr); } /** Checks if a method symbol has any attribute from the org.junit package. */ diff --git a/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java b/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java index e820e00e2b3..b5b0f9ca678 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/TestNgMatchers.java @@ -15,8 +15,8 @@ */ package com.google.errorprone.matchers; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.VisitorState; import com.sun.source.tree.ClassTree; @@ -44,8 +44,7 @@ public static boolean hasTestNgAnnotation(MethodTree tree, VisitorState state) { if (hasTestNgAttr(methodSym)) { return true; } - return findSuperMethods(methodSym, state.getTypes()).stream() - .anyMatch(TestNgMatchers::hasTestNgAttr); + return streamSuperMethods(methodSym, state.getTypes()).anyMatch(TestNgMatchers::hasTestNgAttr); } /** Checks if a class is annotated with any annotation from the org.testng package. */ 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 d56da359ac4..38f354a0c2d 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 @@ -703,11 +703,20 @@ public static MethodSymbol findSuperMethodInType( return null; } + /** + * Finds supermethods of {@code methodSymbol}, not including {@code methodSymbol} itself, and + * including interfaces. + */ public static Set findSuperMethods(MethodSymbol methodSymbol, Types types) { return findSuperMethods(methodSymbol, types, /* skipInterfaces= */ false) .collect(toCollection(LinkedHashSet::new)); } + /** See {@link #findSuperMethods(MethodSymbol, Types)}. */ + public static Stream streamSuperMethods(MethodSymbol methodSymbol, Types types) { + return findSuperMethods(methodSymbol, types, /* skipInterfaces= */ false); + } + private static Stream findSuperMethods( MethodSymbol methodSymbol, Types types, boolean skipInterfaces) { TypeSymbol owner = (TypeSymbol) methodSymbol.owner; diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java index c7853f3093c..e25438fcfaf 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ForOverrideChecker.java @@ -20,8 +20,8 @@ import static com.google.common.collect.Streams.findLast; import static com.google.common.collect.Streams.stream; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static java.util.stream.Stream.concat; import com.google.common.collect.ImmutableList; @@ -199,7 +199,7 @@ private static ImmutableList getOverriddenMethods( "getOverriddenMethods may not be called on a static method"); } - return concat(Stream.of(method), findSuperMethods(method, state.getTypes()).stream()) + return concat(Stream.of(method), streamSuperMethods(method, state.getTypes())) .filter(member -> hasAnnotation(member, FOR_OVERRIDE, state)) .collect(toImmutableList()); } diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java b/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java index 92964a28a44..f9baf7b1923 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/MissingOverride.java @@ -18,8 +18,8 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.StandardTags; @@ -59,7 +59,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { if (ignoreInterfaceOverrides && sym.enclClass().isInterface()) { return NO_MATCH; } - return findSuperMethods(sym, state.getTypes()).stream() + return streamSuperMethods(sym, state.getTypes()) .findFirst() .filter(unused -> ASTHelpers.getGeneratedBy(state).isEmpty()) // to allow deprecated methods to be removed non-atomically, we permit overrides of diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java index 1b260511a78..611fead87fa 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/NoAllocationChecker.java @@ -38,7 +38,7 @@ import static com.google.errorprone.matchers.Matchers.typeCast; import static com.google.errorprone.matchers.Matchers.variableInitializer; import static com.google.errorprone.matchers.Matchers.variableType; -import static com.google.errorprone.util.ASTHelpers.findSuperMethods; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import static com.sun.source.tree.Tree.Kind.AND_ASSIGNMENT; import static com.sun.source.tree.Tree.Kind.DIVIDE_ASSIGNMENT; import static com.sun.source.tree.Tree.Kind.LEFT_SHIFT_ASSIGNMENT; @@ -393,7 +393,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { return NO_MATCH; } MethodSymbol symbol = ASTHelpers.getSymbol(tree); - return findSuperMethods(symbol, state.getTypes()).stream() + return streamSuperMethods(symbol, state.getTypes()) .filter(s -> ASTHelpers.hasAnnotation(s, NoAllocation.class.getName(), state)) .findAny() .map( diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java index 7762bf3f69f..4a6484e731a 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/RestrictedApiChecker.java @@ -21,6 +21,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.errorprone.matchers.Description.NO_MATCH; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.common.collect.ImmutableSet; import com.google.errorprone.BugPattern; @@ -189,15 +190,14 @@ private Description checkMethodUse( } // Try each super method for @RestrictedApi - Optional superWithRestrictedApi = - ASTHelpers.findSuperMethods(method, state.getTypes()).stream() - .filter((t) -> ASTHelpers.hasAnnotation(t, RestrictedApi.class, state)) - .findFirst(); - if (!superWithRestrictedApi.isPresent()) { - return NO_MATCH; - } - return checkRestriction( - getRestrictedApiAnnotation(superWithRestrictedApi.get(), state), where, state); + return streamSuperMethods(method, state.getTypes()) + .filter((t) -> ASTHelpers.hasAnnotation(t, RestrictedApi.class, state)) + .findFirst() + .map( + superWithRestrictedApi -> + checkRestriction( + getRestrictedApiAnnotation(superWithRestrictedApi, state), where, state)) + .orElse(NO_MATCH); } @Nullable diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java b/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java index 1fcf18ebead..e60cf045f17 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/RxReturnValueIgnored.java @@ -22,6 +22,7 @@ import static com.google.errorprone.matchers.Matchers.not; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.hasAnnotation; +import static com.google.errorprone.util.ASTHelpers.streamSuperMethods; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; @@ -66,7 +67,7 @@ private static boolean hasCirvAnnotation(ExpressionTree tree, VisitorState state // if the super-type returned the exact same type. This lets us catch issues where a // superclass was annotated with @CanIgnoreReturnValue but the parent did not intend to // return an Rx type - return ASTHelpers.findSuperMethods(sym, state.getTypes()).stream() + return streamSuperMethods(sym, state.getTypes()) .anyMatch( superSym -> hasAnnotation(superSym, CanIgnoreReturnValue.class, state) From 010bdc2a05f07b96492adc6bce0555c4bd88bb25 Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Tue, 26 Jul 2022 13:25:45 -0700 Subject: [PATCH 088/102] Add additional test cases to CheckReturnValue, related to conditional statements and method references. PiperOrigin-RevId: 463413568 --- .../testdata/CheckReturnValueNegativeCases.java | 10 ++++++++-- .../testdata/CheckReturnValuePositiveCases.java | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java index c9d41052a56..18c872fbb53 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValueNegativeCases.java @@ -36,6 +36,10 @@ private int mustCheck() { return 5; } + private int nothingToCheck() { + return 42; + } + private void callRunnable(Runnable runnable) { runnable.run(); } @@ -51,11 +55,13 @@ private void callSupplier(Supplier supplier) { supplier.get(); } - public void testResolvedToVoidLambda() { + public void testResolvedToIntLambda(boolean predicate) { callSupplier(() -> mustCheck()); + callSupplier(predicate ? () -> mustCheck() : () -> nothingToCheck()); } - public void testMethodReference() { + public void testMethodReference(boolean predicate) { callSupplier(this::mustCheck); + callSupplier(predicate ? this::mustCheck : this::nothingToCheck); } } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java index 65aaab2d21c..1b0ebbfa20f 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/testdata/CheckReturnValuePositiveCases.java @@ -61,7 +61,7 @@ public void testResolvedToVoidLambda() { callRunnable(() -> this.intValue.increment()); } - public void testResolvedToVoidMethodReference() { + public void testResolvedToVoidMethodReference(boolean predicate) { // BUG: Diagnostic contains: The result of `increment()` must be used // // `this.intValue::increment` acts as an implementation of `Runnable.run`. @@ -75,6 +75,8 @@ public void testResolvedToVoidMethodReference() { // If callers of `increment()` shouldn't be required to use its result, then annotate it with // `@CanIgnoreReturnValue`. callRunnable(this.intValue::increment); + // BUG: Diagnostic contains: The result of `increment()` must be used + callRunnable(predicate ? this.intValue::increment : this.intValue::increment2); } public void testConstructorResolvedToVoidMethodReference() { From d19024b059d9db804045fdc992c9f6f3ff073769 Mon Sep 17 00:00:00 2001 From: Kevin Bierhoff Date: Wed, 27 Jul 2022 14:09:52 -0700 Subject: [PATCH 089/102] Enable `UnsafeWildcard` PiperOrigin-RevId: 463675277 --- .../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 d5bb5705d85..184cad3cd19 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -766,6 +766,7 @@ public static ScannerSupplier errorChecks() { UnicodeInCode.class, UnnecessaryCheckNotNull.class, UnnecessaryTypeArgument.class, + UnsafeWildcard.class, UnusedAnonymousClass.class, UnusedCollectionModifiedInPlace.class, Validator.class, @@ -1134,7 +1135,6 @@ public static ScannerSupplier errorChecks() { UnnecessarySetDefault.class, UnnecessaryStaticImport.class, UnsafeLocaleUsage.class, - UnsafeWildcard.class, UnusedException.class, UrlInSee.class, UseEnumSwitch.class, From 3dda6b2ee4ae12533e08d25a3453577ed36944a1 Mon Sep 17 00:00:00 2001 From: Alan Malloy Date: Thu, 28 Jul 2022 14:58:03 -0700 Subject: [PATCH 090/102] Internal commit PiperOrigin-RevId: 463932992 --- .../com/google/errorprone/bugpatterns/inlineme/Inliner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java b/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java index 10885ba8f3b..feb5b8ef9e4 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java @@ -240,7 +240,7 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) { boolean terminalVarargsReplacement = varargsWithEmptyArguments && i == varNames.size() - 1; String capturePrefixForVarargs = terminalVarargsReplacement ? "(?:,\\s*)?" : ""; // We want to avoid replacing a method invocation with the same name as the method. - Pattern extractArgAndNextToken = + var extractArgAndNextToken = Pattern.compile( "\\b" + capturePrefixForVarargs + Pattern.quote(varNames.get(i)) + "\\b([^(])"); String replacementResult = From 36e90e10f641d97fc3a17fb9fd33353aa38bd62b Mon Sep 17 00:00:00 2001 From: ghm Date: Mon, 1 Aug 2022 03:10:42 -0700 Subject: [PATCH 091/102] getThrownExceptions: handle try(var) {} form of try-with-resources too. Fixes external #3321 PiperOrigin-RevId: 464496204 --- .../com/google/errorprone/util/ASTHelpers.java | 18 ++++++++---------- 1 file changed, 8 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 38f354a0c2d..cb14b34bc62 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 @@ -2157,7 +2157,6 @@ public static ImmutableSet getThrownExceptions(Tree tree, VisitorState sta /** Scanner for determining what types are thrown by a tree. */ public static final class ScanThrownTypes extends TreeScanner { - boolean inResources = false; ArrayDeque> thrownTypes = new ArrayDeque<>(); SetMultimap thrownTypesByVariable = HashMultimap.create(); @@ -2215,9 +2214,15 @@ public Void visitTry(TryTree tree, Void unused) { } public void scanResources(TryTree tree) { - inResources = true; + for (Tree resource : tree.getResources()) { + Symbol symbol = getType(resource).tsym; + + if (symbol instanceof ClassSymbol) { + getCloseMethod((ClassSymbol) symbol, state) + .ifPresent(methodSymbol -> getThrownTypes().addAll(methodSymbol.getThrownTypes())); + } + } scan(tree.getResources(), null); - inResources = false; } @Override @@ -2241,13 +2246,6 @@ public Void visitNewClass(NewClassTree tree, Void unused) { @Override public Void visitVariable(VariableTree tree, Void unused) { - if (inResources) { - Symbol symbol = getSymbol(tree.getType()); - if (symbol instanceof ClassSymbol) { - getCloseMethod((ClassSymbol) symbol, state) - .ifPresent(methodSymbol -> getThrownTypes().addAll(methodSymbol.getThrownTypes())); - } - } return super.visitVariable(tree, null); } From 173b0ed7c522dd2e0369e61e9a80de094280f573 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Mon, 1 Aug 2022 14:05:21 -0700 Subject: [PATCH 092/102] Untag UnnecessaryParentheses to exclude it from clrobot style findings PiperOrigin-RevId: 464618990 --- .../google/errorprone/bugpatterns/UnnecessaryParentheses.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java index ff618ae44bc..474d1b4ffa9 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/UnnecessaryParentheses.java @@ -17,7 +17,6 @@ package com.google.errorprone.bugpatterns; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; -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; @@ -36,8 +35,7 @@ summary = "These grouping parentheses are unnecessary; it is unlikely the code will" + " be misinterpreted without them", - severity = WARNING, - tags = STYLE) + severity = WARNING) public class UnnecessaryParentheses extends BugChecker implements ParenthesizedTreeMatcher { @Override From 9449d38d435a9457642d1ea4ee1c35222444ac4d Mon Sep 17 00:00:00 2001 From: Nick Glorioso Date: Mon, 1 Aug 2022 14:38:42 -0700 Subject: [PATCH 093/102] Add the SSTable to the CIRV stats collector. Tested with a local run: https://screenshot.googleplex.com/BKMXArP7HX9ooGD PiperOrigin-RevId: 464626829 --- .../ExternalCanIgnoreReturnValue.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 54a1224df59..520d7d42f5b 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 @@ -37,7 +37,6 @@ import com.sun.tools.javac.util.List; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.stream.Stream; @@ -92,26 +91,25 @@ interface MethodPredicate { enum ConfigParser { AS_STRINGS { @Override - MethodPredicate load(Path file) throws IOException { - return configByInterpretingMethodsAsStrings(MoreFiles.asCharSource(file, UTF_8)); + MethodPredicate load(String file, ErrorProneFlags flags) throws IOException { + return configByInterpretingMethodsAsStrings(MoreFiles.asCharSource(Paths.get(file), UTF_8)); } }, PARSE_TOKENS { @Override - MethodPredicate load(Path file) throws IOException { - return configByParsingApiObjects(MoreFiles.asCharSource(file, UTF_8)); + MethodPredicate load(String file, ErrorProneFlags flags) throws IOException { + return configByParsingApiObjects(MoreFiles.asCharSource(Paths.get(file), UTF_8)); } }; - abstract MethodPredicate load(Path file) throws IOException; + abstract MethodPredicate load(String file, ErrorProneFlags flags) throws IOException; } private static MethodPredicate loadConfigListFromFile(String filename, ErrorProneFlags flags) { ConfigParser configParser = flags.getEnum(EXCLUSION_LIST_PARSER, ConfigParser.class).orElse(ConfigParser.AS_STRINGS); try { - Path file = Paths.get(filename); - return configParser.load(file); + return configParser.load(filename, flags); } catch (IOException e) { throw new UncheckedIOException( "Could not load external resource for CanIgnoreReturnValue", e); From cb1fac88380abef7345ef0ed2e7515e74d0d2dec Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 2 Aug 2022 09:03:29 -0700 Subject: [PATCH 094/102] Fix a crash on a test-only redefinition of ThreadPoolExecutor that doesn't have any parameters PiperOrigin-RevId: 464805116 --- .../bugpatterns/ErroneousThreadPoolConstructorChecker.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java index 4e3877392fd..e32128738da 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ErroneousThreadPoolConstructorChecker.java @@ -64,6 +64,9 @@ public Description matchNewClass(NewClassTree tree, VisitorState state) { return Description.NO_MATCH; } List arguments = tree.getArguments(); + if (arguments.size() < 2) { + return Description.NO_MATCH; + } Integer corePoolSize = ASTHelpers.constValue(arguments.get(0), Integer.class); Integer maximumPoolSize = ASTHelpers.constValue(arguments.get(1), Integer.class); if (corePoolSize == null || maximumPoolSize == null || corePoolSize.equals(maximumPoolSize)) { From e2309dc69901fe123bc3c148b616e8cce8cf2680 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 2 Aug 2022 11:44:18 -0700 Subject: [PATCH 095/102] Extend ReturnsNullCollection to cover Multimap. This matters much less now that ReturnMissingNullable is on, but it's also extremely easy to do. PiperOrigin-RevId: 464846273 --- .../google/errorprone/bugpatterns/ReturnsNullCollection.java | 3 ++- .../errorprone/bugpatterns/ReturnsNullCollectionTest.java | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java index c93831d74db..6326b8a668e 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/ReturnsNullCollection.java @@ -53,7 +53,8 @@ private static boolean methodWithoutNullable(MethodTree tree, VisitorState state allOf( anyOf( methodReturns(isSubtypeOf("java.util.Collection")), - methodReturns(isSubtypeOf("java.util.Map"))), + methodReturns(isSubtypeOf("java.util.Map")), + methodReturns(isSubtypeOf("com.google.common.collect.Multimap"))), ReturnsNullCollection::methodWithoutNullable); public ReturnsNullCollection() { diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java index 11077b3cd9c..7a9876823c1 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/ReturnsNullCollectionTest.java @@ -33,6 +33,7 @@ public void positive() { compilationHelper .addSourceLines( "Test.java", + "import com.google.common.collect.Multimap;", "import java.util.Collection;", "import java.util.ArrayList;", "import java.util.List;", @@ -50,6 +51,10 @@ public void positive() { " // BUG: Diagnostic contains: ReturnsNullCollection", " return null;", " }", + " Multimap methodReturnsNullMultimap() {", + " // BUG: Diagnostic contains: ReturnsNullCollection", + " return null;", + " }", " List methodReturnsNullListConditionally(boolean foo) {", " if (foo) {", " // BUG: Diagnostic contains: ReturnsNullCollection", From a398a3f6bd4620b9304ae3e1ec5c880e20d837ba Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 2 Aug 2022 13:45:55 -0700 Subject: [PATCH 096/102] Fix quadratic behaviour in DisctinctVarargsChecker Do a less sophisticated comparison of source code for distinct entries, which allows us to do set operations instead of calling `ASTHelpers.sameVariable` `n^2` times. PiperOrigin-RevId: 464876981 --- .../bugpatterns/DistinctVarargsChecker.java | 66 ++++++++++--------- .../DistinctVarargsCheckerTest.java | 24 +++++++ 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java b/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java index 13826a20823..16019469a4c 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DistinctVarargsChecker.java @@ -20,17 +20,19 @@ import static com.google.errorprone.matchers.Matchers.anyOf; import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; +import com.google.common.collect.ImmutableListMultimap; 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.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; /** * ErrorProne checker to generate warning when method expecting distinct varargs is invoked with @@ -72,14 +74,14 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState return checkDistinctArgumentsWithFix(tree, state); } if (ALL_DISTINCT_ARG_MATCHER.matches(tree, state)) { - return checkDistinctArguments(tree, tree.getArguments()); + return checkDistinctArguments(state, tree.getArguments()); } if (EVEN_PARITY_DISTINCT_ARG_MATCHER.matches(tree, state)) { List arguments = new ArrayList<>(); for (int index = 0; index < tree.getArguments().size(); index += 2) { arguments.add(tree.getArguments().get(index)); } - return checkDistinctArguments(tree, arguments); + return checkDistinctArguments(state, arguments); } if (EVEN_AND_ODD_PARITY_DISTINCT_ARG_MATCHER.matches(tree, state)) { List evenParityArguments = new ArrayList<>(); @@ -91,29 +93,34 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState oddParityArguments.add(tree.getArguments().get(index)); } } - return checkDistinctArguments(tree, evenParityArguments, oddParityArguments); + return checkDistinctArguments(state, evenParityArguments, oddParityArguments); } return Description.NO_MATCH; } + private static ImmutableListMultimap argumentsByString( + VisitorState state, List arguments) { + ImmutableListMultimap.Builder result = ImmutableListMultimap.builder(); + for (int i = 0; i < arguments.size(); i++) { + result.put(state.getSourceForNode(arguments.get(i)), i); + } + return result.build(); + } + private Description checkDistinctArgumentsWithFix(MethodInvocationTree tree, VisitorState state) { SuggestedFix.Builder suggestedFix = SuggestedFix.builder(); - for (int index = 1; index < tree.getArguments().size(); index++) { - boolean isDistinctArgument = true; - for (int prevElementIndex = 0; prevElementIndex < index; prevElementIndex++) { - if (ASTHelpers.sameVariable( - tree.getArguments().get(index), tree.getArguments().get(prevElementIndex))) { - isDistinctArgument = false; - break; - } - } - if (!isDistinctArgument) { - suggestedFix.merge( - SuggestedFix.replace( - state.getEndPosition(tree.getArguments().get(index - 1)), - state.getEndPosition(tree.getArguments().get(index)), - "")); - } + List arguments = tree.getArguments(); + ImmutableListMultimap argumentsByString = argumentsByString(state, arguments); + for (Map.Entry> entry : argumentsByString.asMap().entrySet()) { + entry.getValue().stream() + .skip(1) + .forEachOrdered( + index -> + suggestedFix.merge( + SuggestedFix.replace( + state.getEndPosition(arguments.get(index - 1)), + state.getEndPosition(arguments.get(index)), + ""))); } if (suggestedFix.isEmpty()) { return Description.NO_MATCH; @@ -122,19 +129,14 @@ private Description checkDistinctArgumentsWithFix(MethodInvocationTree tree, Vis } private Description checkDistinctArguments( - MethodInvocationTree tree, List... argumentsList) { + VisitorState state, List... argumentsList) { for (List arguments : argumentsList) { - for (int firstArgumentIndex = 0; - firstArgumentIndex < arguments.size(); - firstArgumentIndex++) { - for (int secondArgumentIndex = firstArgumentIndex + 1; - secondArgumentIndex < arguments.size(); - secondArgumentIndex++) { - if (ASTHelpers.sameVariable( - arguments.get(firstArgumentIndex), arguments.get(secondArgumentIndex))) { - return describeMatch(tree); - } - } + ImmutableListMultimap argumentsByString = + argumentsByString(state, arguments); + for (Map.Entry> entry : argumentsByString.asMap().entrySet()) { + entry.getValue().stream() + .skip(1) + .forEachOrdered(index -> state.reportMatch(describeMatch(arguments.get(index)))); } } return Description.NO_MATCH; diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java index 0d78edd7d70..ce1f3379113 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DistinctVarargsCheckerTest.java @@ -16,8 +16,11 @@ package com.google.errorprone.bugpatterns; +import static java.util.stream.Collectors.joining; + import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.CompilationTestHelper; +import java.util.stream.IntStream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -195,4 +198,25 @@ public void distinctVarargsChecker_differentVarsInImmutableSetVarargsMethod_shou "}") .doTest(); } + + @Test + public void negative_quadratic() { + + String large = + IntStream.range(0, 7000) + .mapToObj(x -> String.format("\"%s\"", x)) + .collect(joining(", ", "ImmutableSet.of(", ", \"0\");")); + + compilationHelper + .addSourceLines( + "Test.java", + "import com.google.common.collect.ImmutableSet;", + "public class Test {", + " void testFunction() {", + " // BUG: Diagnostic contains: DistinctVarargsChecker", + large, + " }", + "}") + .doTest(); + } } From 8db8e9a89074e493e260c6af474c8633adb3690e Mon Sep 17 00:00:00 2001 From: cpovirk Date: Wed, 3 Aug 2022 12:11:06 -0700 Subject: [PATCH 097/102] Expand `CheckReturnValue` docs. (a small bit of progress in the direction of eventually documenting https://github.com/google/error-prone/issues/3003) PiperOrigin-RevId: 465115966 --- docs/bugpattern/CheckReturnValue.md | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/bugpattern/CheckReturnValue.md b/docs/bugpattern/CheckReturnValue.md index ea879070312..4dfa1b29847 100644 --- a/docs/bugpattern/CheckReturnValue.md +++ b/docs/bugpattern/CheckReturnValue.md @@ -1,3 +1,40 @@ +When code calls a non-`void` method, it should usually use the value that the +method returns. + +Consider the following code, which ignores the return value of `concat`: + +```java +string.concat("\n"); +``` + +That code is a no-op because `concat` doesn't modify `string`; it returns a new +string for the caller to use, as in: + +```java +string = string.concat("\n"); +``` + +To avoid this bug, Error Prone requires callers to use the return value of +`concat` and some other well-known methods. + +Additionally, Error Prone can be configured to require callers to use the return +value of any methods that you choose. + +### How to tell Error Prone which methods to check + +Most methods are like `concat`: Calls to those methods should use their return +values. + +However, there are exceptions. For example, `set.add(element)` returns a +`boolean`: The return value is `false` if `element` was *already* contained in +`set`. Typically, callers don't need to know this, so they don't need to use the +return value. + +For Error Prone's `CheckReturnValue` check to be useful, it needs to know which +methods are like `concat` and which are like `add`. + +#### `@CheckReturnValue` and `@CanIgnoreReturnValue` + The `@CheckReturnValue` annotation (available in JSR-305[^jsr] or in [Error Prone][epcrv]) marks methods whose return values should be checked. This error is triggered when one of these methods is called but the result is not From 33471198f65cbdc44cf754eed83721fd9fdddc85 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 3 Aug 2022 13:27:32 -0700 Subject: [PATCH 098/102] Update release.yml --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de6d7513894..7abbd4fd615 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ jobs: steps: - name: Setup Signing Key run: | + gpgconf --kill gpg-agent gpg-agent --daemon --default-cache-ttl 7200 echo -e "${{ secrets.GPG_SIGNING_KEY }}" | gpg --batch --import --no-tty echo "hello world" > temp.txt From 21eb0cfe84260581e2b2388017a0879c61410cb9 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 3 Aug 2022 13:42:11 -0700 Subject: [PATCH 099/102] Update release.yml --- .github/workflows/release.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7abbd4fd615..be4839042d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,17 +12,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - steps: - - name: Setup Signing Key - run: | - gpgconf --kill gpg-agent - gpg-agent --daemon --default-cache-ttl 7200 - echo -e "${{ secrets.GPG_SIGNING_KEY }}" | gpg --batch --import --no-tty - echo "hello world" > temp.txt - gpg --detach-sig --yes -v --output=/dev/null --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" temp.txt - rm temp.txt - gpg --list-secret-keys --keyid-format LONG - + steps: - name: Checkout uses: actions/checkout@v2.4.0 @@ -35,6 +25,8 @@ jobs: server-id: ossrh server-username: CI_DEPLOY_USERNAME server-password: CI_DEPLOY_PASSWORD + gpg-private-key: ${{ secrets.GPG_PASSPHRASE }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Bump Version Number run: | @@ -51,6 +43,7 @@ jobs: env: CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: mvn --no-transfer-progress -P release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" From 8cb1a77099b4b1c85c67945ca8341c86273cd643 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 3 Aug 2022 13:46:10 -0700 Subject: [PATCH 100/102] Update release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be4839042d5..2024d7deca7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: server-id: ossrh server-username: CI_DEPLOY_USERNAME server-password: CI_DEPLOY_PASSWORD - gpg-private-key: ${{ secrets.GPG_PASSPHRASE }} + gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Bump Version Number From c8c381b01cfa3e2621dfb11d71187d46cc933b60 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 3 Aug 2022 14:09:39 -0700 Subject: [PATCH 101/102] Add gpg configuration suggested by https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#extra-setup-for-pomxml PiperOrigin-RevId: 465144758 --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index e8e765ead4f..058968b7547 100644 --- a/pom.xml +++ b/pom.xml @@ -289,6 +289,12 @@ sign + + + --pinentry-mode + loopback + + From 12aca18e3b9706a77bfa5ee9ffa0e906a981bb06 Mon Sep 17 00:00:00 2001 From: cushon Date: Wed, 3 Aug 2022 22:25:19 +0000 Subject: [PATCH 102/102] Release Error Prone 2.15.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..0eab043dd62 100644 --- a/annotation/pom.xml +++ b/annotation/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 @BugPattern annotation diff --git a/annotations/pom.xml b/annotations/pom.xml index 77a45ae44ea..1bdffd2eca9 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone annotations diff --git a/check_api/pom.xml b/check_api/pom.xml index 4a023c78089..a20e3661e93 100644 --- a/check_api/pom.xml +++ b/check_api/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone check api diff --git a/core/pom.xml b/core/pom.xml index e764a81a419..49a2d07114a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone library diff --git a/docgen/pom.xml b/docgen/pom.xml index a57b821986f..5a6e090a4b1 100644 --- a/docgen/pom.xml +++ b/docgen/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 Documentation tool for generating Error Prone bugpattern documentation diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml index 42915547130..d4c01478c20 100644 --- a/docgen_processor/pom.xml +++ b/docgen_processor/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 JSR-269 annotation processor for @BugPattern annotation diff --git a/pom.xml b/pom.xml index 058968b7547..870d37b63a1 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ Error Prone parent POM com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.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..e6c4ea5e414 100644 --- a/refaster/pom.xml +++ b/refaster/pom.xml @@ -19,7 +19,7 @@ error_prone_parent com.google.errorprone - HEAD-SNAPSHOT + 2.15.0 4.0.0 diff --git a/test_helpers/pom.xml b/test_helpers/pom.xml index 6d798ac870c..48b1f96c812 100644 --- a/test_helpers/pom.xml +++ b/test_helpers/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone test helpers diff --git a/type_annotations/pom.xml b/type_annotations/pom.xml index b2495a2167f..50fa69762ee 100644 --- a/type_annotations/pom.xml +++ b/type_annotations/pom.xml @@ -21,7 +21,7 @@ com.google.errorprone error_prone_parent - HEAD-SNAPSHOT + 2.15.0 error-prone type annotations