GitNexus/gitnexus/test/fixtures/lang-resolution/polyglot-property-isolation/settings.js
ReidenXerx 690470865e fix(scope-resolution): require the return-shape producer to RESOLVE, not merely to name-match
Review finding 2, reached independently by three Claude lanes and two Codex
legs, and reproduced here. `emitReturnShapeMemberAccesses` took the receiver's
type binding, then filtered a WHOLE-GRAPH property index with `idNamesMember` —
a textual match on the node id. Any node whose id happened to read
`<producer>.<member>` qualified, in any file and any language, and it emitted at
the 0.9 PRECISE tier where a `minConfidence` floor cannot filter it out. The
sibling unique-name pass was given a per-language restriction for exactly this
hazard; this pass consumed the same shared index with none.

Three guards, catching different shapes:

  - the producer must RESOLVE to a definition (`findCallableBindingInScope` — a
    CALLABLE lookup: the producer is the function whose return shape owns the
    member, and it resolves through finalized import bindings so a producer in
    another file still yields its own file);
  - the member must live in that definition's file;
  - that file must belong to the language being resolved.

The third is not redundant with the second, which is the part worth recording.
A receiver typed by CONSTRUCTION (`const bound = new Loyalty()`) resolves through
the shared class registry, which is polyglot — so the producer resolves into
`Loyalty.java`, its members legitimately live in that same file, and file
equality waves the cross-language edge straight through.

Also fixes the sibling P2: a site where the receiver IS typed to a producer that
owns no such member now claims the site. That branch is the strongest negative
evidence the pipeline can produce, and letting it fall through meant the 0.5 name
fallback answered a question the precise pass had just DISPROVED — measured,
linking a read to an unrelated same-named key in another file.

`polyglot-property-isolation` gains the bound-receiver arm the review asked for,
and it is the right arm: the pre-existing case has an untyped receiver and so
only ever exercised the unique-name pass, while one extra token routes an
identical read through this one. Mutation-verified — restoring the pre-fix
matching makes exactly the new leak assertion fail. The first version of that arm
was silently vacuous (it introduced a JS key of the same name, which destroyed
the fixture's Java-only premise), which is why it now asserts on the TARGET FILE
rather than on the absence of a name.

KNOWN LIMIT, stated rather than papered over: a member-call producer
(`const r = svc.make()`) binds `svc.make`, which resolves to no callable, so this
pass now declines it. Codex B3 raised that converse case and it is real. Fixing
it means typing `svc` and then finding `make` on that type — a larger piece of
work, queued for the follow-up PR. Declining is the correct interim behaviour:
the alternative is matching `make.<member>` by name across the graph, which is
the fabrication this commit removes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:14:30 +03:00

33 lines
1.5 KiB
JavaScript

// A JS read of the same name through an untyped receiver. Workspace-wide the
// name is unique, so unique-name inference resolved it — to a Java private
// field, across a language boundary with no call path.
export function renderLoyalty(cfg) {
return cfg.loyaltyPointsBalance;
}
// CONTROL: a same-language target the pass SHOULD still reach, so the fix is
// shown to restrict by language rather than to disable the pass.
export const jsConfig = {
jsOnlyThreshold: 10,
};
export function readsJsOnly(bag) {
return bag.jsOnlyThreshold;
}
// BOUND-RECEIVER ARM (review finding 2). The reads above have UNTYPED receivers,
// so they route through unique-name inference — the pass this fixture was
// written to police. One extra token gives the receiver a type and routes an
// identical read through `return-shape-members.ts` instead: a sibling pass that
// consumed the same whole-graph index with no language restriction, and emitted
// at the 0.9 PRECISE tier where a `minConfidence` floor cannot filter it out.
//
// `Loyalty` is declared ONLY in Java. Construction types the receiver through
// the shared (polyglot) class registry, so the producer resolves into
// `Loyalty.java` and its member genuinely lives in that same file — which is
// why a same-FILE check alone waves this through and only a same-LANGUAGE check
// stops it. Nothing here may resolve.
export function readsBoundLoyalty() {
const bound = new Loyalty();
return bound.loyaltyPointsBalance;
}