GitNexus/gitnexus/test/unit/scope-resolution/swift-extension-owners.test.ts
Parafee41 795cf0e151
fix(swift): resolve inherited protocol extension calls (#3309)
* fix(swift): resolve inherited protocol extension calls

* fix(scope): gate inherited implicit receiver lookup

* fix(swift): resolve call result types by exact callee

* test(swift): align cache and local call expectations

* fix(scope): reconcile replay diagnostics

* fix(swift): preserve exact callable return types

* fix(swift): capture throwing async call results

* test(swift): refresh capture golden

* test(swift): refresh scope capture baseline

* fix(swift): require explicit callable returns

* fix(scope): preserve duplicate return metadata

* chore(scope): align index documentation

* Address PR review feedback (#3309)

Stamp only protocol/class-extension members (nested QN + SPM buckets),
arity-narrow implicit-this across MRO, and keep Swift type peeling out
of shared workspace-index via stripTypePreservingDecoration.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3309)

Stamp extension members even when the extension declares a nested type, keep inherited class members ahead of protocol-extension defaults, and report replay-only interface-dispatch fan-out drops.

Note: pre-existing failure in gitnexus tsc against an older gitnexus-shared dist not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3309)

Walk inherited implicit-this owners nearest-first so a nearer override wins, and tighten Swift owner-stamp tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-19 07:44:35 +01:00

198 lines
6.1 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.

/**
* `populateWorkspaceOwners` for Swift must stamp extension members onto a
* unique owner in the same SPM target, skip nested locals, and fail closed
* when `Bar` and `Foo.Bar` both exist.
*/
import { describe, it, expect } from 'vitest';
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
import { swiftScopeResolver } from '../../../src/core/ingestion/languages/swift/scope-resolver.js';
import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
function parseSwift(source: string, filePath: string) {
const parsed = extractParsedFile(swiftScopeResolver.languageProvider, source, filePath, () => {});
if (parsed === undefined) throw new Error('scope extraction failed');
swiftScopeResolver.populateOwners(parsed);
return parsed;
}
function stamp(
files: Parameters<NonNullable<typeof swiftScopeResolver.populateWorkspaceOwners>>[0],
resolutionConfig?: unknown,
) {
swiftScopeResolver.populateWorkspaceOwners?.(files, {
fileContents: new Map(),
resolutionConfig,
});
}
describe.skipIf(!swiftAvailable)('populateSwiftExtensionOwners', () => {
it('stamps protocol-extension methods onto the unique protocol', () => {
const parsed = parseSwift(
`
protocol ScenarioSupport {}
struct Store {}
extension ScenarioSupport {
func makeStore() -> Store { Store() }
}
`,
'Support.swift',
);
const protocol = parsed.localDefs.find((d) => d.qualifiedName === 'ScenarioSupport');
const method = parsed.localDefs.find(
(d) => d.qualifiedName?.split('.').at(-1) === 'makeStore' && d.type === 'Method',
);
expect(protocol).toBeDefined();
expect(method?.ownerId).toBeUndefined();
stamp([parsed]);
expect(method?.ownerId).toBe(protocol?.nodeId);
});
it('does not stamp a nested local onto the enclosing type', () => {
const parsed = parseSwift(
`
class Host {
func helper(_ a: Int) -> Int { a }
func run(_ x: Int) -> Int {
func helper(_ v: Int, _ w: Int) -> Int { v + w }
return helper(x, x)
}
}
`,
'Host.swift',
);
stamp([parsed]);
const twoArg = parsed.localDefs.filter(
(d) => d.type === 'Method' && d.qualifiedName?.endsWith('helper') && d.parameterCount === 2,
);
expect(twoArg).toHaveLength(1);
expect(twoArg[0]?.ownerId).toBeUndefined();
});
it('stamps extension Foo.Bar members onto Foo.Bar when no top-level Bar exists', () => {
const parsed = parseSwift(
`
enum Foo {
struct Bar {}
}
extension Foo.Bar {
func added() {}
}
`,
'Nested.swift',
);
stamp([parsed]);
const added = parsed.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
const bar = parsed.localDefs.find(
(d) => d.qualifiedName === 'Foo.Bar' || d.qualifiedName === 'Bar',
);
expect(added?.ownerId).toBeDefined();
expect(added?.ownerId).toBe(bar?.nodeId);
});
it('fails closed when top-level Bar and nested Foo.Bar both exist', () => {
const parsed = parseSwift(
`
struct Bar {}
enum Foo {
struct Bar {}
}
extension Foo.Bar {
func added() {}
}
`,
'Collide.swift',
);
stamp([parsed]);
const added = parsed.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
expect(added).toBeDefined();
expect(added?.ownerId).toBeUndefined();
});
it('stamps extension members when the extension also declares a nested type', () => {
const parsed = parseSwift(
`
struct Foo {}
extension Foo {
struct Helper {}
func added() {}
}
`,
'NestedInExt.swift',
);
stamp([parsed]);
const added = parsed.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
const foo = parsed.localDefs.find(
(d) => d.qualifiedName === 'Foo' && (d.type === 'Struct' || d.type === 'Class'),
);
expect(added).toBeDefined();
expect(foo).toBeDefined();
expect(added?.ownerId).toBe(foo?.nodeId);
});
it('does not let one target’s Foo own another target’s extension', () => {
const app = parseSwift(
`
protocol Foo {}
extension Foo {
func added() {}
}
`,
'Sources/App/A.swift',
);
const lib = parseSwift(
`
protocol Foo {}
`,
'Sources/Lib/B.swift',
);
stamp([app, lib], {
targets: new Map([
['App', 'Sources/App'],
['Lib', 'Sources/Lib'],
]),
});
const added = app.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
const appFoo = app.localDefs.find((d) => d.qualifiedName === 'Foo');
expect(added).toBeDefined();
expect(appFoo).toBeDefined();
expect(added?.ownerId).toBe(appFoo?.nodeId);
const libFoo = lib.localDefs.find((d) => d.qualifiedName === 'Foo');
expect(added?.ownerId).not.toBe(libFoo?.nodeId);
});
it('stamps a one-line extension whose synthetic class shares the Class start line', () => {
const parsed = parseSwift(
`
struct Foo {}
extension Foo { func added() {} }
`,
'OneLine.swift',
);
const added = parsed.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
const foo = parsed.localDefs.find(
(d) => d.qualifiedName === 'Foo' && (d.type === 'Struct' || d.type === 'Class'),
);
expect(added).toBeDefined();
expect(foo).toBeDefined();
expect(added?.ownerId).toBeUndefined();
stamp([parsed]);
expect(added?.ownerId).toBe(foo?.nodeId);
});
it('stamps a cross-file extension onto the real type, not the synthetic class', () => {
const typeFile = parseSwift('struct Foo {}', 'Foo.swift');
const extFile = parseSwift('extension Foo { func added() {} }', 'Foo+Added.swift');
const added = extFile.localDefs.find((d) => d.qualifiedName?.split('.').at(-1) === 'added');
const foo = typeFile.localDefs.find(
(d) => d.qualifiedName === 'Foo' && (d.type === 'Struct' || d.type === 'Class'),
);
expect(added).toBeDefined();
expect(foo).toBeDefined();
expect(added?.ownerId).toBeUndefined();
stamp([typeFile, extFile]);
expect(added?.ownerId).toBe(foo?.nodeId);
});
});