Skip to content

Commit 941974e

Browse files
cpovirkError Prone Team
authored andcommitted
Extend NullArgumentForNonNullParameter to match calls to some Immutable* methods and to ArgumentCaptor.forClass.
- The `Immutable*` methods of course already throw `NullPointerException`. And while they're already annotated for nullness, they've been ignored by this checker because there are [technical challenges](https://bugs.openjdk.org/browse/JDK-8225377) with identifying the nullness of type-variable types loaded from `.class` files. - `ArgumentCaptor.forClass` soon [will throw `NullPointerException`](mockito/mockito@fe1cb2d#diff-8d274a9bda2d871524d15bbfcd6272bd893a47e6b1a0b460d82a8845615f26daR31). See mockito/mockito#2807 (comment). To match `ArgumentCaptor.forClass`, we have to start checking test-only code. But for now, we check such code _only_ for calls to `ArgumentCaptor.forClass`. (I suppose that we still don't handle the _varargs_ parameters of the various `Immutable*` methods, since this entire check skips varargs, in part because varargs are confusing in general (b/232103314) and in part because we can't see annotations on a varargs-element type that is loaded from a `.class` file. We could make an exception for these methods if we really wanted, but I doubt it's worth it.) PiperOrigin-RevId: 499326542
1 parent c1cb001 commit 941974e

File tree

2 files changed

+98
-9
lines changed

2 files changed

+98
-9
lines changed

core/src/main/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameter.java

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,15 @@ public final class NullArgumentForNonNullParameter extends BugChecker
5757
private static final Supplier<Type> JAVA_OPTIONAL_TYPE = typeFromString("java.util.Optional");
5858
private static final Supplier<Type> GUAVA_OPTIONAL_TYPE =
5959
typeFromString("com.google.common.base.Optional");
60+
private static final Supplier<Type> ARGUMENT_CAPTOR_CLASS =
61+
typeFromString("org.mockito.ArgumentCaptor");
6062
private static final Supplier<Name> OF_NAME = memoize(state -> state.getName("of"));
63+
private static final Supplier<Name> FOR_CLASS_NAME = memoize(state -> state.getName("forClass"));
64+
private static final Supplier<Name> BUILDER_NAME = memoize(state -> state.getName("Builder"));
65+
private static final Supplier<Name> GUAVA_COLLECT_IMMUTABLE_PREFIX =
66+
memoize(state -> state.getName("com.google.common.collect.Immutable"));
67+
private static final Supplier<Name> GUAVA_GRAPH_IMMUTABLE_PREFIX =
68+
memoize(state -> state.getName("com.google.common.graph.Immutable"));
6169
private static final Supplier<Name> COM_GOOGLE_COMMON_PREFIX_NAME =
6270
memoize(state -> state.getName("com.google.common."));
6371

@@ -79,10 +87,6 @@ public Description matchNewClass(NewClassTree tree, VisitorState state) {
7987

8088
private Description match(
8189
MethodSymbol methodSymbol, List<? extends ExpressionTree> args, VisitorState state) {
82-
if (state.errorProneOptions().isTestOnlyTarget()) {
83-
return NO_MATCH; // The tests of `foo` often invoke `foo(null)` to verify that it NPEs.
84-
}
85-
8690
if (hasExtraParameterForEnclosingInstance(methodSymbol)) {
8791
// TODO(b/232103314): Figure out the right way to handle the implicit outer `this` parameter.
8892
return NO_MATCH;
@@ -123,6 +127,24 @@ private Description match(
123127
}
124128

125129
private boolean argumentMustBeNonNull(VarSymbol sym, VisitorState state) {
130+
// We hardcode checking of one test method, ArgumentCaptor.forClass, which throws as of
131+
// https://github.com/mockito/mockito/commit/fe1cb2de0923e78bf7d7ae46cbab792dd4e94136#diff-8d274a9bda2d871524d15bbfcd6272bd893a47e6b1a0b460d82a8845615f26daR31
132+
// For discussion of hardcoding in general, see below.
133+
if (sym.owner.name.equals(FOR_CLASS_NAME.get(state))
134+
&& isParameterOfMethodOnType(sym, ARGUMENT_CAPTOR_CLASS, state)) {
135+
return true;
136+
}
137+
138+
if (state.errorProneOptions().isTestOnlyTarget()) {
139+
return false; // The tests of `foo` often invoke `foo(null)` to verify that it NPEs.
140+
/*
141+
* TODO(cpovirk): But consider still matching *some* cases. For example, we might check
142+
* primitives, since it would be strange to test that `foo(int i)` throws NPE if you call
143+
* `foo((Integer) null)`. And tests that *use* a class like `Optional` (as opposed to
144+
* *testing* Optional) could benefit from checking that they use `Optional.of` correctly.
145+
*/
146+
}
147+
126148
if (sym.asType().isPrimitive()) {
127149
return true;
128150
}
@@ -132,16 +154,31 @@ private boolean argumentMustBeNonNull(VarSymbol sym, VisitorState state) {
132154
* obstacles to relying purely on them, including around type variables (see comments below)—not
133155
* to mention that there are no annotations on JDK classes.
134156
*
135-
* As a workaround, we can hardcode specific APIs that feel worth the effort. For now, the only
136-
* ones we hardcode are the two Optional.of methods. Those just happen to be the ones that I
137-
* thought of and found hits for in our codebase.
157+
* As a workaround, we can hardcode specific APIs that feel worth the effort. Most of the
158+
* hardcoding is below, but one bit is at the top of this method.
138159
*/
160+
161+
// Hardcoding #1: Optional.of
139162
if (sym.owner.name.equals(OF_NAME.get(state))
140163
&& (isParameterOfMethodOnType(sym, JAVA_OPTIONAL_TYPE, state)
141164
|| isParameterOfMethodOnType(sym, GUAVA_OPTIONAL_TYPE, state))) {
142165
return true;
143166
}
144167

168+
// Hardcoding #2: Immutable*.of
169+
if (sym.owner.name.equals(OF_NAME.get(state))
170+
&& (isParameterOfMethodOnTypeStartingWith(sym, GUAVA_COLLECT_IMMUTABLE_PREFIX, state)
171+
|| isParameterOfMethodOnTypeStartingWith(sym, GUAVA_GRAPH_IMMUTABLE_PREFIX, state))) {
172+
return true;
173+
}
174+
175+
// Hardcoding #3: Immutable*.Builder.*
176+
if (sym.enclClass().name.equals(BUILDER_NAME.get(state))
177+
&& (isParameterOfMethodOnTypeStartingWith(sym, GUAVA_COLLECT_IMMUTABLE_PREFIX, state)
178+
|| isParameterOfMethodOnTypeStartingWith(sym, GUAVA_GRAPH_IMMUTABLE_PREFIX, state))) {
179+
return true;
180+
}
181+
145182
/*
146183
* TODO(b/203207989): In theory, we should program this check to exclude inner classes until we
147184
* fix the bug in MoreAnnotations.getDeclarationAndTypeAttributes, which is used by
@@ -188,6 +225,11 @@ private static boolean isParameterOfMethodOnType(
188225
return target != null && state.getTypes().isSameType(sym.enclClass().type, target);
189226
}
190227

228+
private static boolean isParameterOfMethodOnTypeStartingWith(
229+
VarSymbol sym, Supplier<Name> nameSupplier, VisitorState state) {
230+
return sym.enclClass().fullname.startsWith(nameSupplier.get(state));
231+
}
232+
191233
private boolean enclosingAnnotationDefaultsNonTypeVariablesToNonNull(
192234
Symbol sym, VisitorState state) {
193235
for (; sym != null; sym = sym.getEnclosingElement()) {

core/src/test/java/com/google/errorprone/bugpatterns/nullness/NullArgumentForNonNullParameterTest.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,53 @@ public void positiveGuavaOptionalOf() {
111111
.doTest();
112112
}
113113

114+
@Test
115+
public void positiveGuavaImmutableSetOf() {
116+
conservativeHelper
117+
.addSourceLines(
118+
"Foo.java",
119+
"import com.google.common.collect.ImmutableSet;",
120+
"class Foo {",
121+
" void foo() {",
122+
" // BUG: Diagnostic contains: ",
123+
" ImmutableSet.of(null);",
124+
" }",
125+
"}")
126+
.doTest();
127+
}
128+
129+
@Test
130+
public void positiveGuavaImmutableSetBuilderAdd() {
131+
conservativeHelper
132+
.addSourceLines(
133+
"Foo.java",
134+
"import com.google.common.collect.ImmutableSet;",
135+
"class Foo {",
136+
" void foo(boolean b) {",
137+
" // BUG: Diagnostic contains: ",
138+
// We use a ternary to avoid:
139+
// "non-varargs call of varargs method with inexact argument type for last parameter"
140+
" ImmutableSet.builder().add(b ? 1 : null);",
141+
" }",
142+
"}")
143+
.doTest();
144+
}
145+
146+
@Test
147+
public void positiveArgumentCaptorForClass() {
148+
conservativeHelper
149+
.addSourceLines(
150+
"Foo.java",
151+
"import org.mockito.ArgumentCaptor;",
152+
"class Foo {",
153+
" void foo() {",
154+
" // BUG: Diagnostic contains: ",
155+
" ArgumentCaptor.forClass(null);",
156+
" }",
157+
"}")
158+
.doTest();
159+
}
160+
114161
@Test
115162
public void negativeNullMarkedComGoogleCommonButNullable() {
116163
conservativeHelper
@@ -163,10 +210,10 @@ public void negativeNullMarkedTypeVariable() {
163210
aggressiveHelper
164211
.addSourceLines(
165212
"Foo.java",
166-
"import com.google.common.collect.ImmutableSet;",
213+
"import com.google.common.collect.ConcurrentHashMultiset;",
167214
"class Foo {",
168215
" void foo() {",
169-
" ImmutableSet.of(null);",
216+
" ConcurrentHashMultiset.create().add(null);",
170217
" }",
171218
"}")
172219
.doTest();

0 commit comments

Comments
 (0)