mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
* 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>
140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
/**
|
|
* Drift-guard unit test for `groupSwiftFilesBySpmTarget` (issue #1948 U3,
|
|
* KTD2).
|
|
*
|
|
* `groupSwiftFilesBySpmTarget` (`languages/swift/target-grouping.ts`)
|
|
* DUPLICATES the legacy `groupSwiftFilesByTarget` (`languages/swift.ts`)
|
|
* SPM-subtree semantics so the registry-primary same-module hooks group by
|
|
* the SPM target subtree without touching the legacy pipeline (hard
|
|
* constraint: legacy stays byte-identical). Because the duplication can
|
|
* silently drift if legacy is later changed, this test pins the exact
|
|
* bucketing for representative inputs so a future divergence surfaces
|
|
* loudly:
|
|
*
|
|
* 1. A multi-subdir single target buckets into ONE group.
|
|
* 2. A file matching two overlapping same-named target prefixes is
|
|
* assigned to the FIRST target only (legacy `break`s — no fan-out).
|
|
* 3. Unmatched files AND the no-targets case route to `__default__` = all.
|
|
*
|
|
* `coerceSwiftTargets` is also covered: it duck-types `{ targets: Map }`
|
|
* (no `instanceof` on the config object) and returns `null` otherwise.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
groupSwiftFilesBySpmTarget,
|
|
coerceSwiftTargets,
|
|
} from '../../../../src/core/ingestion/languages/swift/target-grouping.js';
|
|
|
|
const id = (s: string) => s;
|
|
|
|
describe('groupSwiftFilesBySpmTarget — legacy SPM-subtree parity (drift guard)', () => {
|
|
it('buckets a multi-subdir single target into ONE group', () => {
|
|
const files = [
|
|
'Sources/Alpha/Core/User.swift',
|
|
'Sources/Alpha/Entry/App.swift',
|
|
'Sources/Alpha/Util/Helpers.swift',
|
|
];
|
|
const targets = new Map([['Alpha', 'Sources/Alpha']]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, targets);
|
|
|
|
expect([...groups.keys()]).toEqual(['Alpha']);
|
|
expect(groups.get('Alpha')).toEqual(files);
|
|
expect(groups.has('__default__')).toBe(false);
|
|
});
|
|
|
|
it('assigns a file matching two overlapping same-named prefixes to the FIRST target only', () => {
|
|
// Both targets are prefixes of the file's path (Beta dir nested under
|
|
// Alpha). Legacy `break`s on the first match → one bucket per file.
|
|
const files = ['Sources/Alpha/Beta/User.swift'];
|
|
const targets = new Map([
|
|
['Alpha', 'Sources/Alpha'],
|
|
['Beta', 'Sources/Alpha/Beta'],
|
|
]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, targets);
|
|
|
|
expect(groups.get('Alpha')).toEqual(files);
|
|
expect(groups.has('Beta')).toBe(false);
|
|
});
|
|
|
|
it('matches a target dir only at a `/` boundary, not a substring', () => {
|
|
// "Sources/Alpha" must NOT match "Sources/AlphaBeta/..." — the legacy
|
|
// predicate requires idx===0 or a preceding `/`.
|
|
const files = ['Sources/AlphaBeta/User.swift'];
|
|
const targets = new Map([['Alpha', 'Sources/Alpha']]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, targets);
|
|
|
|
expect(groups.has('Alpha')).toBe(false);
|
|
expect(groups.get('__default__')).toEqual(files);
|
|
});
|
|
|
|
it('routes unmatched files (with targets present) to __default__', () => {
|
|
const files = ['Sources/Alpha/User.swift', 'Loose/Orphan.swift'];
|
|
const targets = new Map([['Alpha', 'Sources/Alpha']]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, targets);
|
|
|
|
expect(groups.get('Alpha')).toEqual(['Sources/Alpha/User.swift']);
|
|
expect(groups.get('__default__')).toEqual(['Loose/Orphan.swift']);
|
|
});
|
|
|
|
it('routes ALL files to __default__ when targets is null (no source dir found)', () => {
|
|
const files = ['Models/User.swift', 'Services/App.swift'];
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, null);
|
|
|
|
expect([...groups.keys()]).toEqual(['__default__']);
|
|
expect(groups.get('__default__')).toEqual(files);
|
|
});
|
|
|
|
it('routes ALL files to __default__ when targets is empty', () => {
|
|
const files = ['Models/User.swift', 'Services/App.swift'];
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, new Map());
|
|
|
|
expect([...groups.keys()]).toEqual(['__default__']);
|
|
expect(groups.get('__default__')).toEqual(files);
|
|
});
|
|
|
|
it('groups generic items via getPath (not just strings)', () => {
|
|
const items = [
|
|
{ filePath: 'Sources/Alpha/Core/User.swift', tag: 1 },
|
|
{ filePath: 'Sources/Beta/Core/User.swift', tag: 2 },
|
|
];
|
|
const targets = new Map([
|
|
['Alpha', 'Sources/Alpha'],
|
|
['Beta', 'Sources/Beta'],
|
|
]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(items, (i) => i.filePath, targets);
|
|
|
|
expect(groups.get('Alpha')).toEqual([items[0]]);
|
|
expect(groups.get('Beta')).toEqual([items[1]]);
|
|
});
|
|
|
|
it('normalizes backslash paths to forward-slash before matching', () => {
|
|
const files = ['Sources\\Alpha\\Core\\User.swift'];
|
|
const targets = new Map([['Alpha', 'Sources/Alpha']]);
|
|
|
|
const groups = groupSwiftFilesBySpmTarget(files, id, targets);
|
|
|
|
expect(groups.get('Alpha')).toEqual(files);
|
|
});
|
|
});
|
|
|
|
describe('coerceSwiftTargets — duck-type the opaque resolutionConfig', () => {
|
|
it('returns the targets map from a SwiftPackageConfig-shaped object', () => {
|
|
const targets = new Map([['Alpha', 'Sources/Alpha']]);
|
|
expect(coerceSwiftTargets({ targets })).toBe(targets);
|
|
});
|
|
|
|
it('returns null for null / undefined / non-config values', () => {
|
|
expect(coerceSwiftTargets(null)).toBeNull();
|
|
expect(coerceSwiftTargets(undefined)).toBeNull();
|
|
expect(coerceSwiftTargets({})).toBeNull();
|
|
expect(coerceSwiftTargets({ targets: 'not-a-map' })).toBeNull();
|
|
expect(coerceSwiftTargets({ goModule: { modulePath: 'x' } })).toBeNull();
|
|
});
|
|
});
|