diff --git a/gitnexus/src/core/ingestion/languages/dart/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/dart/simple-hooks.ts index 3b8e484ce..fd6161933 100644 --- a/gitnexus/src/core/ingestion/languages/dart/simple-hooks.ts +++ b/gitnexus/src/core/ingestion/languages/dart/simple-hooks.ts @@ -23,6 +23,7 @@ import type { TypeRef, CaptureMatch, } from 'gitnexus-shared'; +import { walkToScope } from '../typescript/simple-hooks.js'; export function dartBindingScopeFor( decl: CaptureMatch, @@ -47,13 +48,7 @@ export function dartBindingScopeFor( // looks (#2807). Gated on the dedicated marker, never on // `@type-binding.constructor` at large, which also fires for genuine locals. if (decl['@type-binding.dart-field'] !== undefined) { - let cur: Scope | undefined = innermost; - while (cur !== undefined) { - if (cur.kind === 'Class') return cur.id; - if (cur.parent === null) break; - cur = tree.getScope(cur.parent); - } - return null; + return walkToScope(innermost, tree, 'Class'); } // (2) Function/method/constructor names are visible in the enclosing scope. diff --git a/gitnexus/src/core/ingestion/utils/call-analysis.ts b/gitnexus/src/core/ingestion/utils/call-analysis.ts index 6281a1fbf..2350626c9 100644 --- a/gitnexus/src/core/ingestion/utils/call-analysis.ts +++ b/gitnexus/src/core/ingestion/utils/call-analysis.ts @@ -574,7 +574,13 @@ const TRANSPARENT_RECEIVER_WRAPPERS = new Set([ * type-preserving. */ const OPERATOR_GATED_RECEIVER_WRAPPERS = new Map([ - ['postfix_expression', '!'], // Swift `self.a!` + // NOT Swift-only: Kotlin's `!!` non-null assertion parses as the same node type + // and is equally type-preserving, so it is peeled too. Measured — the + // receiver-resolution bench moved `kotlin.nonNullAssert` VISIBLE-GAP -> + // RESOLVES when this landed, which is how the Kotlin effect was discovered + // rather than assumed. Any other grammar emitting `postfix_expression` is + // affected as well; the `!` gate, not the language, is what bounds this. + ['postfix_expression', '!'], // Swift `self.a!`, Kotlin `a!!` ]); /** Is `node` a wrapper that denotes exactly what its operand denotes? */ diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index 3cc5b9767..105d4b1c9 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -174,7 +174,19 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j // "pick a bigger number": it is that the check must happen immediately before // merge, because the window between review and merge is exactly when `main` // allocates. Re-check against origin/main before merging this. -const SCHEMA_BUMP = 39; +// v40: inference-typed class fields emit type-binding captures in SIX languages +// (#2807) — TypeScript/JavaScript `public_field_definition|field_definition` with a +// `new_expression` value and `this. = new X()`; Python `self.x = Outer()`; +// Ruby `@ivar = Foo.new`; Swift optional property annotations; Dart inferred-type +// and final field declarations plus constructor-body field writes. Every one of +// these is PARSE-TIME capture emission, so a warm cache replays the pre-fix +// capture set verbatim for byte-unchanged files and the new receiver edges never +// appear — silently, with no error, exactly the v27/v30 failure mode. `analyze` +// skips tree-sitter dispatch for unchanged chunks (GUARDRAILS.md), so a plain +// re-analyze does NOT surface them without this bump. +// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING — main was also at 39 +// when this was allocated, and this file records eight prior collisions. +const SCHEMA_BUMP = 40; const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/test/helpers/temp-dir-pool.ts b/gitnexus/test/helpers/temp-dir-pool.ts index cd6749c82..2c99737a2 100644 --- a/gitnexus/test/helpers/temp-dir-pool.ts +++ b/gitnexus/test/helpers/temp-dir-pool.ts @@ -5,9 +5,16 @@ * these tests each run against a throwaway copy of a fixture. Every consumer * had hand-rolled the SAME three parts — a `string[]` of created dirs, a * `mkdtempSync` that pushes onto it, and an `afterAll` that `rmSync`s the lot. - * Extracted at the fourth consumer (`pipeline-pdg`, `pipeline-pdg-streaming`, - * `interproc-taint`, `pdg-chained-receiver-callees`); the copies had already - * drifted — `pipeline-pdg` registered two cleanup hooks over one array. + * Extracted at the fourth consumer on the branch it came from (`pipeline-pdg`, + * `pipeline-pdg-streaming`, `interproc-taint`, `pdg-chained-receiver-callees`), + * where the copies had already drifted — `pipeline-pdg` registered two cleanup + * hooks over one array. + * + * ON THIS BRANCH it arrives with exactly ONE consumer, + * `pdg-chained-receiver-callees`, which is the only file here that needs it; the + * other three still hand-roll their own cleanup and convert on #2802. The file + * is byte-identical to that branch's copy on purpose, so if both land the add + * resolves as a duplicate rather than a divergence. * * Only the LIFECYCLE is shared, deliberately: seeding differs per test (a * recursive fixture copy, a single file, an inline-written source, or nothing diff --git a/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts b/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts index 7b6f3ab10..69a3c3eac 100644 --- a/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts +++ b/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts @@ -272,6 +272,17 @@ class AssignedField { return r.inner().compute(x); } } + +class ShadowedAssignedField { + var s; + ShadowedAssignedField() { + var s; + s = Outer(); + } + int run(int x) { + return s.inner().compute(x); + } +} `; // ── Swift ──────────────────────────────────────────────────────────────────── @@ -500,6 +511,20 @@ const CASES: readonly LanguageCase[] = [ targets: [`Method:${DART_FILE}:Outer.inner#0`], status: 'resolves', }, + // The shadowing guard, asserted rather than asserted-in-a-comment. Dart + // writes a field with no receiver prefix, so `s = Outer()` is + // syntactically identical to assigning a constructor-local. Here the + // constructor declares its OWN `var s`, so the write targets that local + // and the FIELD must stay untyped — `run` reads the field and must + // therefore resolve nothing. Without the `locals.has(...)` guard in + // `emitDartFieldAssignmentBindings` this row goes green with a WRONG edge, + // which is the failure mode the guard exists to prevent. + { + name: 'shadowed-assigned-field', + callerId: `Method:${DART_FILE}:ShadowedAssignedField.run#1`, + targets: [], + status: 'known-gap', + }, ], }, {