Skip to content

Commit 647e77e

Browse files
authored
Ensure classes null-marked by library model detected in all places (#1197)
Fixes #1194 Previously we were sloppy about allowing a null `Handler` to be passed into `CodeAnnotationInfo` and then caching the result. Here we change the code to always pass a non-null `Handler` in to public APIs of `CodeAnnotationInfo`, and we avoid caching if the handler is ever `null` from an internal call.
1 parent d5cb4f1 commit 647e77e

File tree

10 files changed

+85
-14
lines changed

10 files changed

+85
-14
lines changed

nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ && hasDirectAnnotationWithSimpleName(
115115
public boolean isGenerated(Symbol symbol, Config config) {
116116
Symbol.ClassSymbol classSymbol = enclosingClass(symbol);
117117
if (classSymbol == null) {
118+
// One known case where this can happen: int.class, void.class, etc.
118119
Preconditions.checkArgument(
119-
isClassFieldOfPrimitiveType(
120-
symbol), // One known case where this can happen: int.class, void.class, etc.
120+
isClassFieldOfPrimitiveType(symbol),
121121
String.format(
122122
"Unexpected symbol passed to CodeAnnotationInfo.isGenerated(...) with null enclosing class: %s",
123123
symbol));
@@ -152,7 +152,7 @@ private static boolean isClassFieldOfPrimitiveType(Symbol symbol) {
152152
* @return true if symbol represents an entity contained in a class that is unannotated; false
153153
* otherwise
154154
*/
155-
public boolean isSymbolUnannotated(Symbol symbol, Config config, @Nullable Handler handler) {
155+
public boolean isSymbolUnannotated(Symbol symbol, Config config, Handler handler) {
156156
Symbol.ClassSymbol classSymbol;
157157
if (symbol instanceof Symbol.ClassSymbol) {
158158
classSymbol = (Symbol.ClassSymbol) symbol;
@@ -245,7 +245,11 @@ record = new ClassCacheRecord(recordForEnclosing.outermostClassSymbol, isAnnotat
245245
record =
246246
new ClassCacheRecord(classSymbol, isAnnotatedTopLevelClass(classSymbol, config, handler));
247247
}
248-
classCache.put(classSymbol, record);
248+
// Don't update the cache if the handler is null, as we may not have full info about classes
249+
// being null-marked via library models
250+
if (handler != null) {
251+
classCache.put(classSymbol, record);
252+
}
249253
return record;
250254
}
251255

nullaway/src/main/java/com/uber/nullaway/NullAway.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,7 +2601,7 @@ private boolean mayBeNullExpr(VisitorState state, ExpressionTree expr) {
26012601
"unexpected null symbol for dereference expression " + state.getSourceForNode(expr));
26022602
}
26032603
exprMayBeNull =
2604-
NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config, codeAnnotationInfo);
2604+
NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config, handler, codeAnnotationInfo);
26052605
break;
26062606
case IDENTIFIER:
26072607
if (exprSymbol == null) {
@@ -2610,7 +2610,8 @@ private boolean mayBeNullExpr(VisitorState state, ExpressionTree expr) {
26102610
}
26112611
if (exprSymbol.getKind() == ElementKind.FIELD) {
26122612
exprMayBeNull =
2613-
NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config, codeAnnotationInfo);
2613+
NullabilityUtil.mayBeNullFieldFromType(
2614+
exprSymbol, config, handler, codeAnnotationInfo);
26142615
} else {
26152616
// rely on dataflow analysis for local variables
26162617
exprMayBeNull = true;

nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import com.sun.tools.javac.code.Types;
4949
import com.sun.tools.javac.tree.JCTree;
5050
import com.sun.tools.javac.util.JCDiagnostic;
51+
import com.uber.nullaway.handlers.Handler;
5152
import java.util.List;
5253
import java.util.Map;
5354
import java.util.Set;
@@ -476,10 +477,10 @@ private static boolean isTypeOfNestedClass(Type type) {
476477
* the field might be null; false otherwise
477478
*/
478479
public static boolean mayBeNullFieldFromType(
479-
Symbol symbol, Config config, CodeAnnotationInfo codeAnnotationInfo) {
480+
Symbol symbol, Config config, Handler handler, CodeAnnotationInfo codeAnnotationInfo) {
480481
return !(symbol.getSimpleName().toString().equals("class")
481482
|| symbol.isEnum()
482-
|| codeAnnotationInfo.isSymbolUnannotated(symbol, config, null))
483+
|| codeAnnotationInfo.isSymbolUnannotated(symbol, config, handler))
483484
&& Nullness.hasNullableOrMonotonicNonNullAnnotation(symbol, config);
484485
}
485486

nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,8 @@ public TransferResult<Nullness, NullnessStore> visitFieldAccess(
751751
break;
752752
case UNKNOWN:
753753
fieldMayBeNull =
754-
NullabilityUtil.mayBeNullFieldFromType(symbol, config, getCodeAnnotationInfo(state));
754+
NullabilityUtil.mayBeNullFieldFromType(
755+
symbol, config, handler, getCodeAnnotationInfo(state));
755756
break;
756757
default:
757758
// Should be unreachable unless NullnessHint changes, cases above are exhaustive!

nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ public static Handler buildDefault(Config config) {
5050
// requested it
5151
boolean acknowledgeRestrictive =
5252
config.acknowledgeRestrictiveAnnotations() || config.isJSpecifyMode();
53+
RestrictiveAnnotationHandler restrictiveAnnotationHandler = null;
5354
if (acknowledgeRestrictive) {
5455
// This runs before LibraryModelsHandler, so that library models can override third-party
5556
// bytecode annotations
56-
handlerListBuilder.add(new RestrictiveAnnotationHandler(config));
57+
restrictiveAnnotationHandler = new RestrictiveAnnotationHandler(config);
58+
handlerListBuilder.add(restrictiveAnnotationHandler);
5759
}
5860
if (config.handleTestAssertionLibraries()) {
5961
handlerListBuilder.add(new AssertionHandler(methodNameUtil));
@@ -85,8 +87,15 @@ public static Handler buildDefault(Config config) {
8587
}
8688
handlerListBuilder.add(new LombokHandler(config));
8789
handlerListBuilder.add(new FluentFutureHandler(config));
90+
CompositeHandler mainHandler = new CompositeHandler(handlerListBuilder.build());
8891

89-
return new CompositeHandler(handlerListBuilder.build());
92+
// Initialize the handlers that need to be aware of the main handler
93+
if (restrictiveAnnotationHandler != null) {
94+
restrictiveAnnotationHandler.initMainHandler(mainHandler);
95+
}
96+
libraryModelsHandler.initMainHandler(mainHandler);
97+
98+
return mainHandler;
9099
}
91100

92101
/**

nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import com.uber.nullaway.LibraryModels.MethodRef;
4747
import com.uber.nullaway.NullAway;
4848
import com.uber.nullaway.Nullness;
49+
import com.uber.nullaway.annotations.Initializer;
4950
import com.uber.nullaway.dataflow.AccessPath;
5051
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
5152
import com.uber.nullaway.handlers.stream.StreamTypeRecord;
@@ -73,6 +74,7 @@
7374
public class LibraryModelsHandler extends BaseNoOpHandler {
7475

7576
private final Config config;
77+
private Handler mainHandler;
7678
private final LibraryModels libraryModels;
7779

7880
private @Nullable OptimizedLibraryModels optLibraryModels;
@@ -83,6 +85,11 @@ public LibraryModelsHandler(Config config) {
8385
libraryModels = loadLibraryModels(config);
8486
}
8587

88+
@Initializer
89+
public void initMainHandler(Handler mainHandler) {
90+
this.mainHandler = mainHandler;
91+
}
92+
8693
@Override
8794
public boolean onOverrideFieldNullability(Symbol field) {
8895
return isNullableFieldInLibraryModels(field);
@@ -169,7 +176,8 @@ public boolean onOverrideMayBeNullExpr(
169176
// and any of its overriding implementations.
170177
// see https://github.com/uber/NullAway/issues/445 for why this is needed.
171178
boolean isMethodUnannotated =
172-
getCodeAnnotationInfo(state.context).isSymbolUnannotated(methodSymbol, this.config, null);
179+
getCodeAnnotationInfo(state.context)
180+
.isSymbolUnannotated(methodSymbol, this.config, mainHandler);
173181
if (exprMayBeNull) {
174182
// This is the only case in which we may switch the result from @Nullable to @NonNull:
175183
return !optLibraryModels.hasNonNullReturn(
@@ -226,7 +234,7 @@ public NullnessHint onDataflowVisitMethodInvocation(
226234
AccessPathNullnessPropagation.Updates elseUpdates,
227235
AccessPathNullnessPropagation.Updates bothUpdates) {
228236
boolean isMethodAnnotated =
229-
!getCodeAnnotationInfo(state.context).isSymbolUnannotated(callee, this.config, null);
237+
!getCodeAnnotationInfo(state.context).isSymbolUnannotated(callee, this.config, mainHandler);
230238
setUnconditionalArgumentNullness(bothUpdates, node.getArguments(), callee, state, apContext);
231239
setConditionalArgumentNullness(
232240
thenUpdates, elseUpdates, node.getArguments(), callee, state, apContext);

nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.uber.nullaway.NullAway;
3434
import com.uber.nullaway.NullabilityUtil;
3535
import com.uber.nullaway.Nullness;
36+
import com.uber.nullaway.annotations.Initializer;
3637
import com.uber.nullaway.dataflow.AccessPath;
3738
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
3839
import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
@@ -42,11 +43,17 @@
4243
public class RestrictiveAnnotationHandler extends BaseNoOpHandler {
4344

4445
private final Config config;
46+
private Handler mainHandler;
4547

4648
RestrictiveAnnotationHandler(Config config) {
4749
this.config = config;
4850
}
4951

52+
@Initializer
53+
public void initMainHandler(Handler mainHandler) {
54+
this.mainHandler = mainHandler;
55+
}
56+
5057
/**
5158
* Returns true iff the symbol is considered unannotated but restrictively annotated
5259
* {@code @Nullable} under {@code AcknowledgeRestrictiveAnnotations=true} logic.
@@ -61,7 +68,7 @@ public class RestrictiveAnnotationHandler extends BaseNoOpHandler {
6168
*/
6269
private boolean isSymbolRestrictivelyNullable(Symbol symbol, Context context) {
6370
CodeAnnotationInfo codeAnnotationInfo = getCodeAnnotationInfo(context);
64-
return (codeAnnotationInfo.isSymbolUnannotated(symbol, config, null)
71+
return (codeAnnotationInfo.isSymbolUnannotated(symbol, config, mainHandler)
6572
// with the generated-as-unannotated option enabled, we want to ignore annotations in
6673
// generated code no matter what
6774
&& !(config.treatGeneratedAsUnannotated() && codeAnnotationInfo.isGenerated(symbol, config))

nullaway/src/test/java/com/uber/nullaway/CustomLibraryModelsTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,29 @@ public void libraryModelsAndOverridingFieldNullability() {
264264
"}")
265265
.doTest();
266266
}
267+
268+
@Test
269+
public void issue1194() {
270+
makeLibraryModelsTestHelperWithArgs(
271+
Arrays.asList(
272+
"-d",
273+
temporaryFolder.getRoot().getAbsolutePath(),
274+
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
275+
"-XepOpt:NullAway:JSpecifyMode=true"))
276+
.addSourceLines(
277+
"Test.java",
278+
"package com.uber;",
279+
"import com.uber.lib.unannotated.ProviderNullMarkedViaModel;",
280+
"import org.jspecify.annotations.Nullable;",
281+
"public class Test {",
282+
" void use(Object o) {}",
283+
" void f(Object o) {",
284+
" use(o);",
285+
" // BUG: Diagnostic contains: passing @Nullable parameter",
286+
" use(provider.get());",
287+
" }",
288+
" ProviderNullMarkedViaModel<@Nullable Object> provider = () -> null;",
289+
"}")
290+
.doTest();
291+
}
267292
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.uber.lib.unannotated;
2+
3+
public interface ProviderNullMarkedViaModel<T> {
4+
T get();
5+
}

test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,14 @@ public ImmutableSet<FieldRef> nullableFields() {
132132
fieldRef("com.uber.lib.unannotated.UnannotatedWithModels", "nullableFieldUnannotated2"))
133133
.build();
134134
}
135+
136+
@Override
137+
public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds() {
138+
return ImmutableSetMultimap.of("com.uber.lib.unannotated.ProviderNullMarkedViaModel", 0);
139+
}
140+
141+
@Override
142+
public ImmutableSet<String> nullMarkedClasses() {
143+
return ImmutableSet.of("com.uber.lib.unannotated.ProviderNullMarkedViaModel");
144+
}
135145
}

0 commit comments

Comments
 (0)