mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +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>
122 lines
5.1 KiB
TypeScript
122 lines
5.1 KiB
TypeScript
/**
|
||
* #2807 follow-up — a FUNCTION-LOCAL callable must keep its own graph node when
|
||
* the def and the node disagree about the callable LABEL.
|
||
*
|
||
* Swift's structure phase emits a type's methods as `Function` nodes while the
|
||
* scope extractor derives `Method` from the `@declaration.method` anchor. Every
|
||
* key `resolveDefGraphId` builds is scoped to one label, so under that split the
|
||
* position key (#2699) missed AND the fail-closed guard beside it could never
|
||
* fire — the guard is scoped to the def's own label too, so it was unreachable
|
||
* in exactly the case the sibling-label retry below it serves. The local then
|
||
* fell through to that retry and was aliased onto the class method of the same
|
||
* name.
|
||
*
|
||
* Measured before the fix, on this fixture: the local `func helper` inside
|
||
* `Host.run` resolved to `Function:src/app.swift:Host.helper#1`, so the `sink()`
|
||
* call in the LOCAL's body was emitted as an outgoing edge of the public
|
||
* one-argument method — a caller that does not make that call, present in the
|
||
* graph, in a file whose two `helper`s do not even share an arity.
|
||
*
|
||
* The two `helper`s are deliberately given DIFFERENT arities. Arity is the
|
||
* disambiguator every name-keyed bridge lookup falls back on, so a fixture where
|
||
* they matched could pass on a lookup that still cannot tell the two apart.
|
||
*/
|
||
import { describe, it, expect, beforeAll } from 'vitest';
|
||
import fs from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { getRelationships, runPipelineFromRepo, writeFixtureRepo } from './helpers.js';
|
||
import type { PipelineResult } from './helpers.js';
|
||
import { cleanupTempDirSync } from '../../helpers/test-db.js';
|
||
import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
|
||
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
||
|
||
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
|
||
|
||
const FILE = 'src/app.swift';
|
||
|
||
/**
|
||
* Line/column-sensitive: the local's node id encodes its declaration position
|
||
* (`@8:8` — 0-based row 8, column 8), so reindenting or moving a line renames
|
||
* that node. The inventory test below fails loudly rather than silently
|
||
* measuring nothing if this fixture is edited.
|
||
*/
|
||
const SOURCE = `func sink(_ v: Int) -> Int { return v }
|
||
func probe(_ v: Int) -> Int { return v }
|
||
|
||
class Host {
|
||
func helper(_ a: Int) -> Int {
|
||
return probe(a)
|
||
}
|
||
func run(_ x: Int) -> Int {
|
||
func helper(_ v: Int, _ w: Int) -> Int {
|
||
return sink(v + w)
|
||
}
|
||
return helper(x, x)
|
||
}
|
||
}
|
||
`;
|
||
|
||
const METHOD_HELPER = `Function:${FILE}:Host.helper#1`;
|
||
const LOCAL_HELPER = `Function:${FILE}:Host.run.helper@8:8#2`;
|
||
const RUN = `Function:${FILE}:Host.run#1`;
|
||
const SINK = `Function:${FILE}:sink`;
|
||
const PROBE = `Function:${FILE}:probe`;
|
||
|
||
describe.skipIf(!swiftAvailable)('a function-local callable keeps its own node (#2807)', () => {
|
||
let result: PipelineResult;
|
||
|
||
beforeAll(async () => {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-label-split-'));
|
||
try {
|
||
writeFixtureRepo(dir, { [FILE]: SOURCE });
|
||
// CALLS resolution completes before the graph phases run and nothing here
|
||
// reads what they produce.
|
||
result = await runPipelineFromRepo(dir, () => {}, { skipGraphPhases: true });
|
||
} finally {
|
||
// Not a bare `rmSync`: a pipeline run can still hold a handle open when
|
||
// this fires, which surfaces as EBUSY/EPERM on Windows — `force` does not
|
||
// suppress that — and this suite runs in the sharded Windows CI.
|
||
cleanupTempDirSync(dir);
|
||
}
|
||
}, 120000);
|
||
|
||
/** Distinct CALLS targets emitted by one exact caller id, sorted. */
|
||
const targetsFrom = (callerId: string): string[] =>
|
||
[
|
||
...new Set(
|
||
getRelationships(result, 'CALLS')
|
||
.filter((edge) => edge.rel.sourceId === callerId)
|
||
.map((edge) => edge.rel.targetId),
|
||
),
|
||
].sort();
|
||
|
||
// Non-vacuity: the whole point is that the local and the method are TWO
|
||
// nodes. If the structure phase ever stopped minting the local — or the
|
||
// fixture drifted and renamed it — every edge assertion below would degrade
|
||
// into an empty-vs-empty comparison and pass while measuring nothing.
|
||
it('mints a distinct node for the method and for the function-local', () => {
|
||
expect({
|
||
method: result.graph.getNode(METHOD_HELPER) !== undefined,
|
||
local: result.graph.getNode(LOCAL_HELPER) !== undefined,
|
||
run: result.graph.getNode(RUN) !== undefined,
|
||
}).toEqual({ method: true, local: true, run: true });
|
||
});
|
||
|
||
// The regression itself. Asserted as ONE object over both callers on purpose:
|
||
// the defect moved an edge from one to the other, so checking either side
|
||
// alone would let a fix that merely dropped the edge look correct.
|
||
it('attributes each body’s call to the callable that actually contains it', () => {
|
||
expect({
|
||
[METHOD_HELPER]: targetsFrom(METHOD_HELPER),
|
||
[LOCAL_HELPER]: targetsFrom(LOCAL_HELPER),
|
||
}).toEqual({
|
||
[METHOD_HELPER]: [PROBE],
|
||
[LOCAL_HELPER]: [SINK],
|
||
});
|
||
});
|
||
|
||
it('binds the call in run to the function-local rather than the same-named method', () => {
|
||
expect(targetsFrom(RUN)).toEqual([LOCAL_HELPER]);
|
||
});
|
||
});
|