Files
codegraph/__tests__/kernel-swift-parity.test.ts
T
Colby Mchenry e9be68b61d fix(swift): a composition continued on & lines no longer swallows its type (#2108)
typealias EditorClient = AutocompleteService.Client
      & MediaUploadService.Client

is valid Swift, but in a type's body tree-sitter-swift ends the member at
the newline and the `&` line is an ERROR that swallows the enclosing
type. IceCubesApp's 980-line `EditorStore` came out as loose variables
and functions — no class, no init, no methods — so a call in its init
counted as file-level code ("a file that runs something"), and calls
into its methods from the views landed on same-named methods of other
services.

A Swift preParse moves a line-leading `&` onto the line before, for an
indented `typealias`/`let`/`var` statement's continuation lines only.
Same length and line count; the continuation line's tokens sit one
column left (the Swift type gate reads the site's qualifier at that one
column over). Measured shapes: the failure is type bodies only — a
top-level declaration, a function body and a parenthesized type parse as
written, and are left alone; `&&`, `&+` and inout `&x` are never
touched. The preParse is hoisted before the kernel call like the C/C#
ones, so both arms parse the same bytes, and the file no longer defers
to wasm.

Across IceCubesApp, SwiftPackageIndex-Server, swift-nio, Alamofire, TCA
and mastodon-ios (3,000 Swift files) it rewrites exactly the one failing
file, which now parses clean; none newly erroring. EditorStore is a class
again with 38 methods; views now reference it and call its
insertStatusText / runAssistant (62 edges into it), and
`urlSession.upload` no longer lands on MediaUploadService.upload.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:43:56 +00:00

150 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* Kernel↔wasm Swift extraction parity (R7b of the kernel migration).
*
* Asserts the native walker (codegraph-kernel/src/swift.rs) produces the SAME
* ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
* unresolved refs compared as canonicalized multisets — over the checked-in
* torture fixture (torture.swift: the DEDICATED in-class property branch
* (#1020 — computed→property with getter walk, static let/var→constant/
* variable, stored→field, owner-attributed decorator/type/attr-arg refs,
* observed-property field + class-attributed observer calls), extensions
* (multi-segment resolveName, sugar `[Proto]` names, where-clauses),
* everything-is-extends inheritance, the full call matrix (subscript reads,
* `defer`, optional-chaining receivers, #750 re-encode, literal-set
* membership quirks, implicit members), positional return types with the
* nested-generic failure, present-false isAsync, `open`→internal visibility,
* multi-case enum first-only minting, `/** *​/` docs ignored-and-chain-
* breaking, value-ref targets incl. the declared-then-assigned
* assignment-prune case the swift-nio sweep caught, SWIFT_SPEC fn-refs with
* the label-forward skip and #selector shapes) and its CRLF variant (derived
* in-memory — #1329).
*
* The full-repo sweep lives in scripts/kernel-parity.mjs (Alamofire/vapor/
* swift-nio, --max-deferral 0.3 — swift error incidence is structurally
* 9–27% on BOTH arms); this suite keeps the invariant alive in `npm test`.
* Skips when no kernel binary is staged; CODEGRAPH_KERNEL_EXPECT=1 turns
* that into a failure (kernel-scaffold.test.ts).
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult } from '../src/types';
const KERNEL_PATH = path.join(
__dirname,
'..',
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
);
const kernelBuilt = fs.existsSync(KERNEL_PATH);
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'kernel-parity');
function canon(result: ExtractionResult): { nodes: string[]; edges: string[]; refs: string[] } {
return {
nodes: result.nodes
.map(({ updatedAt: _u, ...n }) => JSON.stringify(n, Object.keys(n).sort()))
.sort(),
edges: result.edges.map((e) => JSON.stringify(e, Object.keys(e).sort())).sort(),
refs: result.unresolvedReferences
.map((r) => JSON.stringify(r, Object.keys(r).sort()))
.sort(),
};
}
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
let savedEnv: Record<string, string | undefined>;
describe.skipIf(!kernelBuilt)('kernel Swift extraction parity', () => {
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['swift']);
});
beforeEach(() => {
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
resetKernelForTests();
});
afterEach(() => {
for (const k of ENV_KEYS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
resetKernelForTests();
});
function assertParity(filePath: string, source: string, minNodes = 3): void {
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
delete process.env.CODEGRAPH_KERNEL;
const viaKernel = tryKernelExtract(filePath, source, 'swift');
expect(viaKernel, `kernel extraction failed for ${filePath}`).not.toBeNull();
process.env.CODEGRAPH_KERNEL = '0';
const viaWasm = extractFromSource(filePath, source, 'swift');
delete process.env.CODEGRAPH_KERNEL;
const k = canon(viaKernel!);
const w = canon(viaWasm);
expect(k.nodes, `${filePath}: nodes`).toEqual(w.nodes);
expect(k.edges, `${filePath}: edges`).toEqual(w.edges);
expect(k.refs, `${filePath}: refs`).toEqual(w.refs);
expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
}
it('torture fixture: property branch, extensions, call matrix, value refs, fn-refs', () => {
const file = path.join(FIXTURE_DIR, 'torture.swift');
assertParity('fixtures/torture.swift', fs.readFileSync(file, 'utf8'), 40);
});
// CRLF variant — the shape every Windows autocrlf checkout has. Derived in
// memory so no platform or editor can silently normalize it away; pins the
// JS-multiline-^ docstring semantics for `///` runs (#1329).
it('torture fixture CRLF parity', () => {
const file = path.join(FIXTURE_DIR, 'torture.swift');
const crlf = fs.readFileSync(file, 'utf8').replace(/(?<!\r)\n/g, '\r\n');
assertParity('fixtures/torture.swift (crlf)', crlf, 40);
});
// A composition's `&` line in a type body errored and swallowed the type;
// the Swift preParse moves the `&` up a line, hoisted so both arms parse the
// same bytes — and the file no longer defers.
it('a composition typealias in a type body parses on both arms, LF and CRLF', () => {
const source = [
'extension StatusEditor {',
' final class EditorStore: NSObject {',
' typealias EditorClient = AutocompleteService.Client',
' & PostingService.Client',
' & CustomEmojiService.Client',
' let service: any Uploading',
' & Describing',
' func post() -> Bool { validate() }',
' func validate() -> Bool { true }',
' }',
'}',
'',
].join('\n');
assertParity('src/EditorStore.swift', source, 5);
assertParity('src/EditorStore.swift (crlf)', source.replace(/\n/g, '\r\n'), 5);
});
it('files with parse errors defer to the wasm extractor (recovery is encoding-dependent)', () => {
// A NEW-only regression construct (`#if` between enum cases — the swift
// checklist's grammar-bump delta 5) — errors on the 0.7.3 grammar.
const broken = 'enum E {\n case a\n#if DEBUG\n case b\n#endif\n}\n';
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
delete process.env.CODEGRAPH_KERNEL;
expect(tryKernelExtract('src/Broken.swift', broken, 'swift')).toBeNull();
process.env.CODEGRAPH_KERNEL = '0';
const viaWasm = extractFromSource('src/Broken.swift', broken, 'swift');
delete process.env.CODEGRAPH_KERNEL;
expect(viaWasm.nodes.some((n) => n.kind === 'file')).toBe(true);
});
});