BaseTypeValidator.validateEnclosingTypeArgs (added in #1920) reports an
out-of-bound enclosing type argument once per nesting level that sits
inside it, instead of once total, for a chain three or more levels deep.
Minimal repro
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
class DoubleReportRepro {
static class A<XXX extends @NonNull Object> {
class B<YYY extends @NonNull Object> {
class C {}
}
}
void field(A<@Nullable String>.B<@NonNull String>.C p) {}
}
Actual output
DoubleReportRepro.java:11: error: [type.argument.type.incompatible] incompatible type argument for type parameter XXX of A.
void field(A<@Nullable String>.B<@NonNull String>.C p) {}
^
found : @Nullable String
required: @NonNull Object
DoubleReportRepro.java:11: error: [type.argument.type.incompatible] incompatible type argument for type parameter XXX of A.
void field(A<@Nullable String>.B<@NonNull String>.C p) {}
^
found : @Nullable String
required: @NonNull Object
2 errors
The same violation (XXX of A, @Nullable String vs. @NonNull Object) is
reported twice, once anchored under A<@Nullable String> and once anchored
under B<@NonNull String>. YYY (B's own type parameter) is satisfied and
correctly produces no report on its own.
Expected output
One error, anchored under A<@Nullable String>.
Root cause
AnnotatedTypeScanner.visitDeclared recurses into a declared type's
enclosing type as part of its normal traversal, passing the same p
(here, the same Tree) down unchanged:
// framework/src/main/java/org/checkerframework/framework/type/visitor/AnnotatedTypeScanner.java
if (type.getEnclosingType() != null) {
r = scan(type.getEnclosingType(), p);
...
}
So for A<@Nullable String>.B<@NonNull String>.C, BaseTypeValidator .visitDeclared is invoked three times — once for C, once (via the
scanner's own recursion) for B<@NonNull String>, and once for
A<@Nullable String> — every time with the same original Tree.
validateEnclosingTypeArgs doesn't account for this: on every one of those
invocations it independently walks the entire outward qualifier chain
starting from the passed-in tree, rather than checking only the one
enclosing level that corresponds to the type it was actually called with.
So the C-level call's walk already reaches and checks A; the B-level
call (triggered separately by the scanner's own recursion into C's
enclosing type) walks outward from the same tree and reaches A again,
producing the second report. A two-level chain (Outer<...>.Inner) doesn't
show this, because there's only one enclosing level to walk and only one
scanner recursion reaches it — the duplication only appears from three
levels of nesting on.
Suggested fix direction
validateEnclosingTypeArgs likely shouldn't walk the outward chain itself
at all: the scanner's own scan(type.getEnclosingType(), p) recursion
already visits each enclosing level in turn. Checking only the immediate
enclosing level (type.getEnclosingType(), one visitParameterizedType
call, no loop) on each visitDeclared invocation should be sufficient and
would let the scanner's existing recursion supply the multi-level walk for
free, instead of validateEnclosingTypeArgs re-implementing it and
overlapping with it. Not verified end-to-end; flagging as the likely
direction rather than a confirmed fix.
Notes
BaseTypeValidator.validateEnclosingTypeArgs(added in #1920) reports anout-of-bound enclosing type argument once per nesting level that sits
inside it, instead of once total, for a chain three or more levels deep.
Minimal repro
Actual output
The same violation (
XXX of A,@Nullable Stringvs.@NonNull Object) isreported twice, once anchored under
A<@Nullable String>and once anchoredunder
B<@NonNull String>.YYY(B's own type parameter) is satisfied andcorrectly produces no report on its own.
Expected output
One error, anchored under
A<@Nullable String>.Root cause
AnnotatedTypeScanner.visitDeclaredrecurses into a declared type'senclosing type as part of its normal traversal, passing the same
p(here, the same
Tree) down unchanged:So for
A<@Nullable String>.B<@NonNull String>.C,BaseTypeValidator .visitDeclaredis invoked three times — once forC, once (via thescanner's own recursion) for
B<@NonNull String>, and once forA<@Nullable String>— every time with the same originalTree.validateEnclosingTypeArgsdoesn't account for this: on every one of thoseinvocations it independently walks the entire outward qualifier chain
starting from the passed-in tree, rather than checking only the one
enclosing level that corresponds to the
typeit was actually called with.So the
C-level call's walk already reaches and checksA; theB-levelcall (triggered separately by the scanner's own recursion into
C'senclosing type) walks outward from the same tree and reaches
Aagain,producing the second report. A two-level chain (
Outer<...>.Inner) doesn'tshow this, because there's only one enclosing level to walk and only one
scanner recursion reaches it — the duplication only appears from three
levels of nesting on.
Suggested fix direction
validateEnclosingTypeArgslikely shouldn't walk the outward chain itselfat all: the scanner's own
scan(type.getEnclosingType(), p)recursionalready visits each enclosing level in turn. Checking only the immediate
enclosing level (
type.getEnclosingType(), onevisitParameterizedTypecall, no loop) on each
visitDeclaredinvocation should be sufficient andwould let the scanner's existing recursion supply the multi-level walk for
free, instead of
validateEnclosingTypeArgsre-implementing it andoverlapping with it. Not verified end-to-end; flagging as the likely
direction rather than a confirmed fix.
Notes
masterfor the field/parameter position, whichBaseTypeValidator: check enclosing types' type arguments against their bounds #1920 already covers; not specific to the extends-clause/local-variable
fix in TypeFromTypeTreeVisitor: restore written enclosing type arguments of qualified types #1925.
description for the pointer that led here.