fix(cache): bump SCHEMA_BUMP for the six-language capture change, + review fixes

SCHEMA_BUMP 39 -> 40. THIS IS THE MERGE-BLOCKER of the review: every language
change in this PR is PARSE-TIME capture emission, and `analyze` skips tree-sitter
dispatch for byte-unchanged chunks (GUARDRAILS.md:34), so a warm cache replays
the pre-fix capture set verbatim and the new receiver edges never appear —
silently, no error. Exactly the v27/v30 failure mode this file already documents.
The PR description's claim that "no schema or version constant applies" was
wrong on both counts: a bump IS required, and a plain re-analyze does NOT
surface the captures without it. Re-check against origin/main before merging —
main was also at 39 when 40 was allocated, and this file records eight prior
collisions.

Also from the review:

- dart/simple-hooks.ts hand-rolled a 9-line parent walk byte-identical to the
  shared `walkToScope(innermost, tree, 'Class')` that TypeScript and Ruby call
  in one line in this same PR. Now uses the helper.
- utils/call-analysis.ts: the doc framed the postfix-`!` peel as Swift-only. It
  is not — Kotlin `!!` parses as the same node and is peeled too, which the
  receiver-resolution bench proved (kotlin.nonNullAssert VISIBLE-GAP ->
  RESOLVES). The comment now says so, and names the `!` gate rather than the
  language as the bound.
- test/helpers/temp-dir-pool.ts: its doc claimed four consumers; on THIS branch
  only `pdg-chained-receiver-callees` uses it (the other three convert on
  #2802). Corrected, and the byte-identical-to-#2802 intent recorded.
- inferred-field-receiver-matrix: adds the Dart shadowing assertion the header
  comment already CLAIMED to make but never did. First attempt was vacuous —
  `var s = Outer()` is a declaration, so it never produced the bare
  `assignment_expression` the guard inspects; removing the guard did not fail
  the row. Fixture corrected to `var s; s = Outer();`, and mutation-verified:
  guard present 35 pass, guard removed the row goes red.

Refs #2807

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-08-03 17:10:20 +00:00
parent fc90d94f06
commit 0418b0aac1
5 changed files with 57 additions and 12 deletions

View file

@ -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.

View file

@ -574,7 +574,13 @@ const TRANSPARENT_RECEIVER_WRAPPERS = new Set([
* type-preserving.
*/
const OPERATOR_GATED_RECEIVER_WRAPPERS = new Map<string, string>([
['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? */

View file

@ -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.<field> = 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

View file

@ -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

View file

@ -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',
},
],
},
{