mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
* 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>
198 lines
6.1 KiB
TypeScript
198 lines
6.1 KiB
TypeScript
/**
|
||
* `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);
|
||
});
|
||
});
|