GitNexus/gitnexus/test/integration/swift-scope-capture-tripwire.test.ts
Gergő Magyar 7d40156003
feat(swift): migrate Swift to scope-based registry resolution (#937) (#1948)
* feat(swift): migrate Swift to scope-based registry resolution (#937)

Ring 3 of RFC #909 — Swift is the final language migrated to the
scope-based registry resolution pipeline. Flips Swift into
MIGRATED_LANGUAGES so registry-primary call resolution is the production
default, with dual-mode parity proven: the resolver suite passes 77/77
under both the legacy DAG (REGISTRY_PRIMARY_SWIFT=0) and the
registry-primary path (REGISTRY_PRIMARY_SWIFT=1).

New language module src/core/ingestion/languages/swift/ (mirrors csharp/):
query, captures, interpret, import-decomposer, receiver-binding,
signature-bindings, arity (+metadata), merge-bindings, simple-hooks,
import-target, target-siblings, implicit-imports, sibling-type-bindings,
scope-resolver, cache-stats, index. Parse-time hooks wired into the
existing flat languages/swift.ts (coexists with the swift/ dir, like
kotlin) and the resolver registered in the scope-resolution registry.

tree-sitter-swift 0.7.1 specifics handled in the Swift module (not in
shared code):
- class / struct / extension all parse to class_declaration; extensions
  are re-keyed onto the extended type so members hoist (like C# partial).
- if-let / guard-let have no if_let_binding node — the optional binding
  is synthesized from if_statement / guard_statement.
- the name: field is reused for func name, param labels, param types and
  return type, so param/return type-bindings are synthesized in code
  (signature-bindings.ts) rather than via a multi-name query.
- no `new` keyword: Type(...) and Type.init(...) are synthesized into
  constructor type-bindings.

Shared-pipeline additions are language-agnostic (AGENTS.md: no language
names in shared ingestion code):
- constructorCallTargetsClass on the ScopeResolver contract +
  free-call-fallback option + run.ts wiring: when true, Type(...) links
  to the Class def rather than its explicit init Constructor.
- pickUniqueGlobalClass: constructor-branch global fallback for
  cross-file types absent from the call site's lexical bindings, deduped
  by qualifiedName so extension/partial fragments aren't seen as
  ambiguous.
- emitImplicitImportEdges: same-module File->File IMPORTS edges (Swift
  whole-module visibility has no syntactic import to drive the generic
  ImportEdge pipeline).

Import resolution: rewrote the O(n^2) module scan in
import-resolvers/configs/swift.ts with a WeakMap-memoized index.

Benchmarks & guards:
- Swift added to bench/scope-capture/measure.mjs + baselines.json
  (fingerprint + 1.5x scaling budget); `--check` passes for all 7
  languages, Swift scaling 0.98 (linear).
- golden capture-parity test
  (test/unit/scope-resolution/swift/swift-captures-golden.test.ts +
  fixtures/swift-captures-golden/) mirrors the csharp golden.
- O(n) scope-capture tripwire
  (test/integration/swift-scope-capture-tripwire.test.ts).

Full Swift test glob: 3 files / 88 tests pass; tsc --noEmit clean; no
cross-language resolver regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(swift): green CI — Dart canary, cascade-safe availability test, prettier, comment/order nits (U1)

* test(swift): wire createResolverParityIt('swift') + empty legacy skip-set (U2)

* fix(swift): group same-module files by SPM target subtree in registry-primary hooks (U3)

* fix(swift): correct member-write, class-func self, multi-clause if-let, nested-extension (U4)

* perf(scope-resolution): build global class index once for pickUniqueGlobalClass (U5)

* fix(swift): re-baseline scope-capture fingerprint after member-write capture change (U4)

* style(swift): prettier-format pick-unique-global-class test (U5 follow-up)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-31 16:56:47 +01:00

80 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Tripwire: emitSwiftScopeCaptures must stay O(n) in entity count.
*
* #1848 (Go) was an O(n²) scope-capture regression: each capture re-derived
* its node via findNodeAtRange(tree.rootNode, …), so capture extraction went
* quadratic on big files. Swift threads the captured node through directly
* (issue #937, RFC #909 Ring 3). This guards against a re-introduction by
* asserting the per-entity cost stays roughly flat across a 4× size increase.
*
* Complements bench/scope-capture/measure.mjs (which pins a fingerprint + a
* 1.5× scaling budget for the `--check` CI job): this always-on test fails the
* normal suite — no GITNEXUS_BENCH gate, no committed baseline — if the path
* regresses to quadratic.
*
* Swift is an optional dependency; skips gracefully if the grammar isn't built.
*
* Kept out of the unit suite's tight timeout: lives in integration where a few
* hundred ms of generated-source parsing is acceptable. Pattern mirrors
* test/integration/csharp-scope-capture-tripwire.test.ts.
*/
import { describe, it, expect } from 'vitest';
import { emitSwiftScopeCaptures } from '../../src/core/ingestion/languages/swift/index.js';
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
/** Generate a Swift source file with `n` DAO-style classes. */
function generateSource(n: number): string {
const classes: string[] = [];
for (let i = 0; i < n; i++) {
classes.push(
`class Entity${i} {\n` +
` var id: Int64 = 0\n` +
` var name: String = ""\n` +
` func getId() -> Int64 { return self.id }\n` +
` func setName(_ v: String) { self.name = v }\n` +
`}`,
);
}
return classes.join('\n\n');
}
function median(xs: number[]): number {
const s = [...xs].sort((a, b) => a - b);
const m = Math.floor(s.length / 2);
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
}
/** Median elapsed ms to run `emitSwiftScopeCaptures` over `reps` runs. */
function timeEmit(n: number, reps: number): number {
const src = generateSource(n);
// Warm up parser/query compilation so the first run's JIT cost is excluded.
emitSwiftScopeCaptures(src, 'warmup.swift');
const samples: number[] = [];
for (let i = 0; i < reps; i++) {
const start = process.hrtime.bigint();
emitSwiftScopeCaptures(src, `bench-${n}.swift`);
samples.push(Number(process.hrtime.bigint() - start) / 1e6);
}
return median(samples);
}
describe.skipIf(!swiftAvailable)('swift scope-capture scaling (O(n) tripwire)', () => {
it('emits at least one capture per entity', () => {
const matches = emitSwiftScopeCaptures(generateSource(250), 'count.swift');
expect(matches.length).toBeGreaterThan(250);
});
it('per-entity cost stays roughly flat from 250 to 1000 entities', () => {
const reps = 5;
const tSmall = timeEmit(250, reps);
const tLarge = timeEmit(1000, reps);
// Linear would be ~4× (4× the entities). Allow generous headroom for noise
// and parser variance; quadratic would be ~16×, so 8× cleanly separates them.
const ratio = tLarge / Math.max(tSmall, 0.001);
expect(ratio).toBeLessThan(8);
});
});