mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-12 08:41:34 +00:00
* fix(swift,dart): type chained receivers through hoisted return types (#3498) Swift and Dart hoist every method's return-type binding out of the class body onto the Module scope, as C# does, but neither set hoistTypeBindingsToModule. The receiver walk therefore never looked there, and svc.getUser().save() had no receiver type. Both now set it. The flag alone was not safe. The hoisted bindings are keyed by bare method name, so with class A { getUser() -> User } and class B { getUser() -> Admin } in one file the Module scope keeps only B's entry, and a.getUser().save() resolved to Admin.save. C# and TypeScript already do this on main. So there is a new ScopeResolver toggle, hoistedTypeBindingsRequireOwner, which Swift and Dart also set. With it, the walk accepts a hoisted binding only when its declaration anchor encloses the declaration of a member of that name owned by the receiver's class. Otherwise it keeps climbing, and declines if nothing passes. The four walk-ups in compound-receiver.ts now share one helper. The toggle is opt-in. With the guard on everywhere, TypeScript loses correct edges where two same-named methods happen to return the same type (11 rows in inferred-field-receiver-matrix), because the Module map cannot hold both. Swift and Dart never had those edges. Bench: swift plainChain, plainDeepChain, optionalChain and nonNullAssert (INVISIBLE-GAP) and dart plainChain, plainDeepChain and optionalChain (VISIBLE-GAP) now RESOLVE with correct targets. No other cell and no count-arm figure moves. The inferred-field matrix gains correct Inner.compute edges for Swift (3 rows) and Dart (7 Alien rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(scope): guard hoisted return ownership * fix(scope): gate hoisted return replay checks * fix(scope): preserve parameterized return replay Keep exact callee return facts when ancestor candidates have matching known parameter types, while refusing same-owner overload ties. Refs #3666 * fix(scope): preserve known variadic return replay Compare complete captured parameter vectors across distinct ancestor owners, including variadic methods with unknown numeric arity. Keep unknown Swift parameter slots instead of compressing them away. Refs #3666 * test(swift): retain reviewed receiver fixtures * fix(scope): retain unknown overload candidates Keep unknown parameter slots possible without hiding conversion overloads. Check return-owner precision without requiring valid calls to stay unresolved. Refs #3498 * fix(scope): preserve conversion suppression Keep explicit empty conversion results final when parameter metadata is unknown. Add exact positive controls for receiver-fixture liveness. Refs #3498 --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.7 KiB
TypeScript
170 lines
6.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
|
import { TransitionalScopeTree } from '../../src/storage/scope-index-store.js';
|
|
import {
|
|
isLanguageAvailable,
|
|
loadParser,
|
|
loadLanguage,
|
|
} from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
let dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
|
|
if (dartAvailable) {
|
|
try {
|
|
await loadParser();
|
|
await loadLanguage(SupportedLanguages.Dart);
|
|
} catch {
|
|
dartAvailable = false;
|
|
}
|
|
}
|
|
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
|
|
const swift = `class User { func save() {} }
|
|
class Admin { func save() {} }
|
|
class Service { func getUser() -> User { return User() } }
|
|
func probe(svc: Service) { svc.getUser().save() }
|
|
`;
|
|
const dart = `class User { void save() {} }
|
|
class Admin { void save() {} }
|
|
class Service { User getUser() => User(); }
|
|
void probe(Service svc) { svc.getUser().save(); }
|
|
`;
|
|
describe('hoisted receiver review regressions', () => {
|
|
for (const [label, file, source, available] of [
|
|
['Swift', 'Models.swift', swift, swiftAvailable],
|
|
['Dart', 'models.dart', dart, dartAvailable],
|
|
] as const) {
|
|
it.skipIf(!available)(
|
|
`${label} preserves exact edges after the disk seal`,
|
|
async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-hoisted-disk-'));
|
|
const store = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-hoisted-store-'));
|
|
const previous = process.env.GITNEXUS_DISK_SCOPE_INDEX;
|
|
const seal = vi.spyOn(TransitionalScopeTree.prototype, 'seal');
|
|
try {
|
|
await fs.writeFile(path.join(root, file), source);
|
|
const outputs: string[][] = [];
|
|
for (const disk of [false, true]) {
|
|
process.env.GITNEXUS_DISK_SCOPE_INDEX = disk ? '1' : '0';
|
|
seal.mockClear();
|
|
const result = await runPipelineFromRepo(root, () => {}, {
|
|
parsedFileStorePath: path.join(store, disk ? 'disk' : 'resident'),
|
|
});
|
|
expect(seal).toHaveBeenCalledTimes(disk ? 1 : 0);
|
|
const calls = [...result.graph.iterRelationships()]
|
|
.filter(
|
|
(e) =>
|
|
e.type === 'CALLS' &&
|
|
result.graph.getNode(e.sourceId)?.properties.name === 'probe' &&
|
|
result.graph.getNode(e.targetId)?.properties.name === 'save',
|
|
)
|
|
.map((e) => e.targetId)
|
|
.sort();
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]).toContain('User.save');
|
|
outputs.push(calls);
|
|
}
|
|
expect(outputs[1]).toEqual(outputs[0]);
|
|
} finally {
|
|
seal.mockRestore();
|
|
if (previous === undefined) delete process.env.GITNEXUS_DISK_SCOPE_INDEX;
|
|
else process.env.GITNEXUS_DISK_SCOPE_INDEX = previous;
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
await fs.rm(store, { recursive: true, force: true });
|
|
}
|
|
},
|
|
120000,
|
|
);
|
|
}
|
|
it.skipIf(!swiftAvailable)(
|
|
'does not borrow a nearer overload return for an inherited call',
|
|
async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-swift-inherited-overload-'));
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(root, 'Models.swift'),
|
|
`class User { func save() {} }
|
|
class Admin { func save() {} }
|
|
class Base { func getUser() -> User { return User() } }
|
|
class Child: Base { func getUser(_ id: Int) -> Admin { return Admin() } }
|
|
func probe(svc: Child) { svc.getUser().save() }
|
|
func probeBound(svc: Child) { let user = svc.getUser(); user.save() }
|
|
func probeControl(user: User) { user.save() }
|
|
`,
|
|
);
|
|
const result = await runPipelineFromRepo(root, () => {}, {});
|
|
// Check owner precision while intermediate argument evidence is limited.
|
|
const saveCalls = [...result.graph.iterRelationships()].filter(
|
|
(e) =>
|
|
e.type === 'CALLS' &&
|
|
['probe', 'probeBound'].includes(
|
|
result.graph.getNode(e.sourceId)?.properties.name ?? '',
|
|
) &&
|
|
e.targetId.includes('.save'),
|
|
);
|
|
for (const call of saveCalls) {
|
|
expect(call.targetId).toBe('Function:Models.swift:User.save#0');
|
|
}
|
|
// This supported call checks fixture-local resolution, not completeness
|
|
// of the primary calls or coverage of every selective primary-path loss.
|
|
const controlCalls = [...result.graph.iterRelationships()]
|
|
.filter(
|
|
(e) =>
|
|
e.type === 'CALLS' &&
|
|
result.graph.getNode(e.sourceId)?.properties.name === 'probeControl' &&
|
|
e.targetId.includes('.save'),
|
|
)
|
|
.map((e) => e.targetId)
|
|
.sort();
|
|
expect(controlCalls).toEqual(['Function:Models.swift:User.save#0']);
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
120000,
|
|
);
|
|
it.skipIf(!dartAvailable)(
|
|
'does not confuse a tear-off invocation with its return value call',
|
|
async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-dart-tearoff-'));
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(root, 'models.dart'),
|
|
`class Admin { void save() {} }
|
|
class User { Admin call() => Admin(); void save() {} }
|
|
class Service { User getUser() => User(); }
|
|
void probe(Service svc) { svc.getUser.call().save(); }
|
|
void probeControl(User user) { user.save(); }
|
|
`,
|
|
);
|
|
const result = await runPipelineFromRepo(root, () => {}, {});
|
|
const saveCalls = [...result.graph.iterRelationships()].filter(
|
|
(e) =>
|
|
e.type === 'CALLS' &&
|
|
result.graph.getNode(e.sourceId)?.properties.name === 'probe' &&
|
|
e.targetId.includes('.save'),
|
|
);
|
|
// Check owner precision; callable-return flow is not modeled yet.
|
|
for (const call of saveCalls) {
|
|
expect(call.targetId).toBe('Method:models.dart:User.save#0');
|
|
}
|
|
// This supported call checks fixture-local resolution, not completeness
|
|
// of the primary call or coverage of every selective primary-path loss.
|
|
const controlCalls = [...result.graph.iterRelationships()]
|
|
.filter(
|
|
(e) =>
|
|
e.type === 'CALLS' &&
|
|
result.graph.getNode(e.sourceId)?.properties.name === 'probeControl' &&
|
|
e.targetId.includes('.save'),
|
|
)
|
|
.map((e) => e.targetId)
|
|
.sort();
|
|
expect(controlCalls).toEqual(['Method:models.dart:User.save#0']);
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
120000,
|
|
);
|
|
});
|