Skip to content

BaseTypeValidator.validateEnclosingTypeArgs double-reports a 3+-level enclosing-chain violation #1926

Description

@wmdietl

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions