Skip to content

Commit 9272d8b

Browse files
authored
Bug fix with type substitutions after inference (#1277)
Fixes #1267 Due to possible type mutations, e.g., when substituting for bounds, sometimes we see `TypeVar`s in types that are not the same object as in the original method type for which we performed inference. Previously, we wouldn't substitute properly in such cases. Here, we collect all `TypeVar`s with the same `Symbol` observed during inference and substitute for all of them, fixing the problem. This makes the Caffeine integration test green again. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved nullability handling for generics: all matching occurrences of a type variable are now annotated with a synthetic nullable marker, ensuring consistent analysis and avoiding missed substitutions across repeated type-variable occurrences. * **Tests** * Updated tests to reflect improved generic inference behavior; removed prior suppression/workarounds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 66e9f57 commit 9272d8b

File tree

3 files changed

+155
-17
lines changed

3 files changed

+155
-17
lines changed

nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -657,15 +657,26 @@ private Type substituteInferredNullabilityForTypeVariables(
657657
for (Map.Entry<Element, ConstraintSolver.InferredNullability> entry :
658658
typeVarNullability.entrySet()) {
659659
if (entry.getValue() == NULLABLE) {
660-
Type curTypeVar = (Type) entry.getKey().asType();
661-
typeVars.append(curTypeVar);
662-
inferredTypes.append(
663-
TypeSubstitutionUtils.typeWithAnnot(
664-
curTypeVar, GenericsChecks.getSyntheticNullAnnotType(state)));
660+
// find all TypeVars occurring in targetType with the same symbol and substitute for those.
661+
// we can have multiple such TypeVars due to previous substitutions that modified the type
662+
// in some way, e.g., by changing its bounds
663+
Element symbol = entry.getKey();
664+
TypeVarWithSymbolCollector tvc = new TypeVarWithSymbolCollector(symbol);
665+
targetType.accept(tvc, null);
666+
for (Type.TypeVar tv : tvc.getMatches()) {
667+
typeVars.append(tv);
668+
inferredTypes.append(
669+
TypeSubstitutionUtils.typeWithAnnot(tv, getSyntheticNullAnnotType(state)));
670+
}
665671
}
666672
}
667-
return TypeSubstitutionUtils.subst(
668-
state.getTypes(), targetType, typeVars.toList(), inferredTypes.toList(), config);
673+
com.sun.tools.javac.util.List<Type> typeVarsToReplace = typeVars.toList();
674+
if (!typeVarsToReplace.isEmpty()) {
675+
return TypeSubstitutionUtils.subst(
676+
state.getTypes(), targetType, typeVarsToReplace, inferredTypes.toList(), config);
677+
} else {
678+
return targetType;
679+
}
669680
}
670681

671682
/**
@@ -1437,6 +1448,8 @@ public boolean isNullableAnnotated(Type type) {
14371448
return Nullness.hasNullableAnnotation(type.getAnnotationMirrors().stream(), config);
14381449
}
14391450

1451+
private @Nullable Type syntheticNullAnnotType;
1452+
14401453
/**
14411454
* Returns a "fake" {@link Type} object representing a synthetic {@code @Nullable} annotation.
14421455
*
@@ -1450,12 +1463,15 @@ public boolean isNullableAnnotated(Type type) {
14501463
* Symtab}.
14511464
* @return a fake {@code Type} for a synthetic {@code @Nullable} annotation.
14521465
*/
1453-
public static Type getSyntheticNullAnnotType(VisitorState state) {
1454-
Names names = Names.instance(state.context);
1455-
Symtab symtab = Symtab.instance(state.context);
1456-
Name name = names.fromString("nullaway.synthetic");
1457-
Symbol.PackageSymbol packageSymbol = new Symbol.PackageSymbol(name, symtab.noSymbol);
1458-
Name simpleName = names.fromString("Nullable");
1459-
return new Type.ErrorType(simpleName, packageSymbol, Type.noType);
1466+
private Type getSyntheticNullAnnotType(VisitorState state) {
1467+
if (syntheticNullAnnotType == null) {
1468+
Names names = Names.instance(state.context);
1469+
Symtab symtab = Symtab.instance(state.context);
1470+
Name name = names.fromString("nullaway.synthetic");
1471+
Symbol.PackageSymbol packageSymbol = new Symbol.PackageSymbol(name, symtab.noSymbol);
1472+
Name simpleName = names.fromString("Nullable");
1473+
syntheticNullAnnotType = new Type.ErrorType(simpleName, packageSymbol, Type.noType);
1474+
}
1475+
return syntheticNullAnnotType;
14601476
}
14611477
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.uber.nullaway.generics;
2+
3+
import com.sun.tools.javac.code.Type;
4+
import com.sun.tools.javac.code.Type.ArrayType;
5+
import com.sun.tools.javac.code.Type.CapturedType;
6+
import com.sun.tools.javac.code.Type.ClassType;
7+
import com.sun.tools.javac.code.Type.ForAll;
8+
import com.sun.tools.javac.code.Type.MethodType;
9+
import com.sun.tools.javac.code.Type.TypeVar;
10+
import com.sun.tools.javac.code.Type.WildcardType;
11+
import com.sun.tools.javac.code.Types;
12+
import java.util.Collections;
13+
import java.util.IdentityHashMap;
14+
import java.util.LinkedHashSet;
15+
import java.util.Set;
16+
import javax.lang.model.element.Element;
17+
import org.jspecify.annotations.Nullable;
18+
19+
/**
20+
* Visitor that collects every TypeVar whose symbol is the given Element.
21+
*
22+
* <p>Usage: TypeVarWithSymbolCollector v = new TypeVarWithSymbolCollector(elem);
23+
* rootType.accept(v,null); Set<TypeVar> matches = v.getMatches();
24+
*
25+
* <p>Not safe to run multiple times; create a fresh visitor for each root type to scan.
26+
*/
27+
public final class TypeVarWithSymbolCollector extends Types.DefaultTypeVisitor<Void, Void> {
28+
29+
private final Element symbol;
30+
private final Set<TypeVar> matches = new LinkedHashSet<>();
31+
32+
public TypeVarWithSymbolCollector(Element symbol) {
33+
this.symbol = symbol;
34+
}
35+
36+
private final Set<Type> seen = java.util.Collections.newSetFromMap(new IdentityHashMap<>());
37+
38+
/** Walk a (possibly null) type. */
39+
private void scan(@Nullable Type t) {
40+
if (t != null && seen.add(t)) {
41+
t.accept(this, null);
42+
}
43+
}
44+
45+
/** Results (unmodifiable). */
46+
public Set<TypeVar> getMatches() {
47+
return Collections.unmodifiableSet(matches);
48+
}
49+
50+
// ---- Core matching logic ----
51+
@Override
52+
public Void visitTypeVar(TypeVar t, Void p) {
53+
if (t.tsym == symbol) {
54+
matches.add(t);
55+
}
56+
scan(t.getUpperBound());
57+
scan(t.getLowerBound());
58+
return null;
59+
}
60+
61+
// ---- Common container types ----
62+
@Override
63+
public Void visitClassType(ClassType t, Void p) {
64+
for (Type arg : t.getTypeArguments()) {
65+
scan(arg);
66+
}
67+
// For inner classes, the enclosing type may also carry args.
68+
scan(t.getEnclosingType());
69+
return null;
70+
}
71+
72+
@Override
73+
public Void visitArrayType(ArrayType t, Void p) {
74+
scan(t.getComponentType());
75+
return null;
76+
}
77+
78+
@Override
79+
public Void visitWildcardType(WildcardType t, Void p) {
80+
scan(t.getExtendsBound());
81+
scan(t.getSuperBound());
82+
return null;
83+
}
84+
85+
@Override
86+
public Void visitCapturedType(CapturedType t, Void p) {
87+
scan(t.getUpperBound());
88+
scan(t.getLowerBound());
89+
scan(t.wildcard);
90+
return null;
91+
}
92+
93+
// ---- Functional / executable types ----
94+
@Override
95+
public Void visitMethodType(MethodType t, Void p) {
96+
scan(t.getReturnType());
97+
for (Type pt : t.getParameterTypes()) {
98+
scan(pt);
99+
}
100+
for (Type thrown : t.getThrownTypes()) {
101+
scan(thrown);
102+
}
103+
return null;
104+
}
105+
106+
@Override
107+
public Void visitForAll(ForAll t, Void p) {
108+
scan(t.qtype);
109+
for (Type type : t.getTypeArguments()) {
110+
scan(type);
111+
}
112+
return null;
113+
}
114+
115+
// ---- Trivial / leaf types we don't need to descend into ----
116+
@Override
117+
public Void visitType(Type t, Void p) {
118+
// Fallback: best-effort traversal via type arguments, if any.
119+
for (Type arg : t.getTypeArguments()) {
120+
scan(arg);
121+
}
122+
return null;
123+
}
124+
}

nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,9 +1110,7 @@ public void nullableWildcardFromCaffeine() {
11101110
" void test() {",
11111111
" JCacheLoaderAdapter<K, V> adapter = new JCacheLoaderAdapter<>();",
11121112
" caffeine.<K, @Nullable Expirable<V>>build(adapter);",
1113-
" // TODO inference is buggy for this case; doesn't substitute inferred types properly",
1114-
" // See https://github.com/uber/NullAway/issues/1267",
1115-
" @SuppressWarnings(\"NullAway\")",
1113+
" // also works with inference",
11161114
" Object o = caffeine.build(adapter);",
11171115
" }",
11181116
" }",

0 commit comments

Comments
 (0)