mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +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>
190 lines
8.7 KiB
TypeScript
190 lines
8.7 KiB
TypeScript
/**
|
|
* Unit tests for `pickUniqueGlobalClass` + `buildGlobalClassIndex` — the
|
|
* constructor-form global class fallback in `free-call-fallback.ts`.
|
|
*
|
|
* U5 (Swift remediation, 2026-05-31) replaced a per-call-site
|
|
* `scopes.defs.byId.values()` rescan with a once-built `simpleName ->
|
|
* class-like defs` index, making each constructor-form fallback site O(1)
|
|
* instead of O(|defs|). The refactor is behavior-PRESERVING for all 8
|
|
* `allowGlobalFreeCallFallback` languages (c, cpp, go, javascript, php, ruby,
|
|
* rust, swift) — the only observable change is performance.
|
|
*
|
|
* These tests exercise the helpers via synthetic `SymbolDefinition` stubs — no
|
|
* fixtures, no pipeline — mirroring the `pick-implicit-this-overload.test.ts`
|
|
* precedent. They assert:
|
|
* - the resolution contract (unique / same-qualifiedName-keep-first /
|
|
* distinct-qualifiedName-ambiguous);
|
|
* - the `Class | Struct | Interface` kind filter, including KEEP-`Interface`
|
|
* (KTD5 — a future drop of `Interface` is a deliberate test-breaking
|
|
* change, not an accident);
|
|
* - equivalence with a reference linear scan, which guards the O(n)->O(1)
|
|
* ordering invariant the refactor relies on.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import type { SymbolDefinition } from 'gitnexus-shared';
|
|
import {
|
|
buildGlobalClassIndex,
|
|
pickUniqueGlobalClass,
|
|
} from '../../../src/core/ingestion/scope-resolution/passes/free-call-fallback.js';
|
|
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
|
|
|
const mkDef = (overrides: Partial<SymbolDefinition> & { nodeId: string }): SymbolDefinition => ({
|
|
filePath: 'x.swift',
|
|
type: 'Class',
|
|
qualifiedName: overrides.nodeId,
|
|
...overrides,
|
|
});
|
|
|
|
/** Wrap a flat def list as the `scopes.defs.byId` map `buildGlobalClassIndex`
|
|
* iterates. Insertion order is preserved by `Map`, so this reproduces the
|
|
* `defs.byId.values()` iteration order the old per-site scan walked. */
|
|
const mkScopes = (defs: readonly SymbolDefinition[]): ScopeResolutionIndexes =>
|
|
({
|
|
defs: {
|
|
byId: new Map(defs.map((d) => [d.nodeId, d])),
|
|
},
|
|
}) as unknown as ScopeResolutionIndexes;
|
|
|
|
/** Reference O(|defs|) linear scan — the pre-U5 `pickUniqueGlobalClass` body,
|
|
* kept verbatim so the equivalence test can prove the index path matches it
|
|
* for every scenario. */
|
|
const referenceLinearScan = (
|
|
name: string,
|
|
scopes: ScopeResolutionIndexes,
|
|
): SymbolDefinition | undefined => {
|
|
let found: SymbolDefinition | undefined;
|
|
for (const def of scopes.defs.byId.values()) {
|
|
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
|
|
const qualified = def.qualifiedName;
|
|
if (qualified === undefined || qualified.length === 0) continue;
|
|
const dot = qualified.lastIndexOf('.');
|
|
const simple = dot === -1 ? qualified : qualified.slice(dot + 1);
|
|
if (simple !== name) continue;
|
|
if (found !== undefined && found.qualifiedName !== def.qualifiedName) return undefined;
|
|
if (found === undefined) found = def;
|
|
}
|
|
return found;
|
|
};
|
|
|
|
const resolve = (name: string, defs: readonly SymbolDefinition[]): SymbolDefinition | undefined =>
|
|
pickUniqueGlobalClass(name, buildGlobalClassIndex(mkScopes(defs)));
|
|
|
|
describe('pickUniqueGlobalClass — once-built global class index (U5)', () => {
|
|
it('returns the def for a unique simple-name match', () => {
|
|
const user = mkDef({ nodeId: 'def:User', qualifiedName: 'app.User' });
|
|
const result = resolve('User', [user]);
|
|
expect(result?.nodeId).toBe('def:User');
|
|
});
|
|
|
|
it('keeps the first fragment when matches share one qualifiedName (extension / partial)', () => {
|
|
// Same logical type re-keyed across extension / partial-class fragments —
|
|
// they resolve to the same graph node, so this is NOT ambiguous.
|
|
const main = mkDef({ nodeId: 'def:User#1', qualifiedName: 'app.User' });
|
|
const ext = mkDef({ nodeId: 'def:User#2', qualifiedName: 'app.User' });
|
|
const result = resolve('User', [main, ext]);
|
|
expect(result?.nodeId).toBe('def:User#1');
|
|
});
|
|
|
|
it('returns undefined for a distinct-qualifiedName collision (ambiguous)', () => {
|
|
// Two genuinely distinct types sharing a simple name → unresolved rather
|
|
// than guessing.
|
|
const a = mkDef({ nodeId: 'def:a.User', qualifiedName: 'a.User' });
|
|
const b = mkDef({ nodeId: 'def:b.User', qualifiedName: 'b.User' });
|
|
const result = resolve('User', [a, b]);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('does not index non-class-like kinds (Function/Method/Constructor/Enum/Record)', () => {
|
|
const defs: SymbolDefinition[] = [
|
|
mkDef({ nodeId: 'def:fn', type: 'Function', qualifiedName: 'app.Foo' }),
|
|
mkDef({ nodeId: 'def:m', type: 'Method', qualifiedName: 'app.Foo' }),
|
|
mkDef({ nodeId: 'def:ctor', type: 'Constructor', qualifiedName: 'app.Foo' }),
|
|
mkDef({ nodeId: 'def:enum', type: 'Enum', qualifiedName: 'app.Foo' }),
|
|
mkDef({ nodeId: 'def:rec', type: 'Record', qualifiedName: 'app.Foo' }),
|
|
];
|
|
const index = buildGlobalClassIndex(mkScopes(defs));
|
|
expect(index.has('Foo')).toBe(false);
|
|
expect(pickUniqueGlobalClass('Foo', index)).toBeUndefined();
|
|
});
|
|
|
|
it('resolves a Struct match', () => {
|
|
const point = mkDef({ nodeId: 'def:Point', type: 'Struct', qualifiedName: 'geo.Point' });
|
|
expect(resolve('Point', [point])?.nodeId).toBe('def:Point');
|
|
});
|
|
|
|
it('resolves an Interface match (locks in KEEP-Interface — KTD5)', () => {
|
|
// KTD5: Interface stays in the filter for the behavior-preserving 8-lang
|
|
// refactor. A future protocol-exclusion change must break THIS test
|
|
// deliberately rather than silently.
|
|
const proto = mkDef({
|
|
nodeId: 'def:Drawable',
|
|
type: 'Interface',
|
|
qualifiedName: 'ui.Drawable',
|
|
});
|
|
expect(resolve('Drawable', [proto])?.nodeId).toBe('def:Drawable');
|
|
});
|
|
|
|
it('skips defs with empty or undefined qualifiedName', () => {
|
|
const defs: SymbolDefinition[] = [
|
|
mkDef({ nodeId: 'def:empty', qualifiedName: '' }),
|
|
mkDef({ nodeId: 'def:undef', qualifiedName: undefined }),
|
|
];
|
|
const index = buildGlobalClassIndex(mkScopes(defs));
|
|
expect(index.size).toBe(0);
|
|
expect(pickUniqueGlobalClass('', index)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for a missing-name lookup', () => {
|
|
const user = mkDef({ nodeId: 'def:User', qualifiedName: 'app.User' });
|
|
expect(resolve('Nonexistent', [user])).toBeUndefined();
|
|
});
|
|
|
|
it('keys by the last dotted segment, not the full qualifiedName', () => {
|
|
// Deeply-qualified name — lookup is by simple name only.
|
|
const deep = mkDef({ nodeId: 'def:deep', qualifiedName: 'a.b.c.Widget' });
|
|
expect(resolve('Widget', [deep])?.nodeId).toBe('def:deep');
|
|
expect(resolve('a.b.c.Widget', [deep])).toBeUndefined();
|
|
});
|
|
|
|
it('matches an undotted qualifiedName by its whole value', () => {
|
|
const bare = mkDef({ nodeId: 'def:Bare', qualifiedName: 'Bare' });
|
|
expect(resolve('Bare', [bare])?.nodeId).toBe('def:Bare');
|
|
});
|
|
|
|
it('equivalence: index result == reference linear scan for every scenario', () => {
|
|
// One combined corpus mixing class-like kinds, excluded kinds, same- and
|
|
// distinct-qualifiedName collisions, and skipped defs — exercised across a
|
|
// battery of names. The index path must agree with the linear scan on
|
|
// BOTH the resolved nodeId and the ambiguous/miss (undefined) outcome,
|
|
// which is what guards the O(n)->O(1) ordering invariant.
|
|
const corpus: SymbolDefinition[] = [
|
|
mkDef({ nodeId: 'def:User#1', qualifiedName: 'app.User' }),
|
|
mkDef({ nodeId: 'def:fn', type: 'Function', qualifiedName: 'app.User' }),
|
|
mkDef({ nodeId: 'def:User#2', qualifiedName: 'app.User' }), // same-qn fragment
|
|
mkDef({ nodeId: 'def:a.Item', qualifiedName: 'a.Item' }),
|
|
mkDef({ nodeId: 'def:b.Item', qualifiedName: 'b.Item' }), // distinct-qn collision
|
|
mkDef({ nodeId: 'def:Point', type: 'Struct', qualifiedName: 'geo.Point' }),
|
|
mkDef({ nodeId: 'def:Drawable', type: 'Interface', qualifiedName: 'ui.Drawable' }),
|
|
mkDef({ nodeId: 'def:Enumish', type: 'Enum', qualifiedName: 'app.Enumish' }),
|
|
mkDef({ nodeId: 'def:empty', qualifiedName: '' }),
|
|
mkDef({ nodeId: 'def:undef', qualifiedName: undefined }),
|
|
];
|
|
const scopes = mkScopes(corpus);
|
|
const index = buildGlobalClassIndex(scopes);
|
|
const names = [
|
|
'User', // same-qn keep-first
|
|
'Item', // distinct-qn ambiguous
|
|
'Point', // struct
|
|
'Drawable', // interface
|
|
'Enumish', // excluded kind → miss
|
|
'Missing', // miss
|
|
'', // skipped-qn → miss
|
|
];
|
|
for (const name of names) {
|
|
const fromIndex = pickUniqueGlobalClass(name, index);
|
|
const fromScan = referenceLinearScan(name, scopes);
|
|
expect(fromIndex?.nodeId).toBe(fromScan?.nodeId);
|
|
}
|
|
});
|
|
});
|