GitNexus/gitnexus/test/integration/resolvers/swift-local-vs-method-label-split.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

122 lines
5.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.

/**
* #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]);
});
});