GitNexus/gitnexus/test/unit/parsedfile-store.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

1047 lines
42 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.

import { describe, it, expect, vi } from 'vitest';
import { promises as nodeFsPromises, existsSync } from 'node:fs';
import v8 from 'node:v8';
import { mkdtemp, rm, readdir, readFile, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import path from 'path';
import type { ParsedFile } from 'gitnexus-shared';
import {
clearParsedFileStore,
persistParsedFileChunk,
persistParsedFileShardSync,
persistDurableParsedFileShardSync,
durableChunkHasShards,
loadParsedFilesForPaths,
getParsedFileStoreDir,
getDurableParsedFileDir,
parsedFileLoadGc,
prepareDurableParsedFileChunk,
pruneAndSaveDurableParsedFileStore,
mergeStagedDurableParsedFileStore,
loadDurableParsedFileIndex,
} from '../../src/storage/parsedfile-store.js';
/**
* Build a minimal ParsedFile whose Scope carries `bindings` / `typeBindings`
* Maps — the round-trip's fidelity hinges on those Maps surviving JSON
* serialization (they would otherwise collapse to `{}`).
*/
const makeParsedFile = (filePath: string): ParsedFile =>
({
filePath,
moduleScope: `${filePath}:module`,
parsedImports: [],
localDefs: [
{ nodeId: `Function:${filePath}:fn`, filePath, type: 'Function', qualifiedName: 'fn' },
],
referenceSites: [],
scopes: [
{
id: `${filePath}:module`,
parent: null,
kind: 'Module',
range: { startLine: 1, startCol: 0, endLine: 9, endCol: 0 },
filePath,
bindings: new Map([['fn', [{ defId: `Function:${filePath}:fn`, origin: 'local' }]]]),
ownedDefs: [],
imports: [],
typeBindings: new Map([['x', { name: 'int' }]]),
},
],
}) as unknown as ParsedFile;
/**
* Store payload with arbitrary (possibly corrupt) field overrides. The one
* controlled escape hatch for building malformed serialization-boundary
* fixtures lives HERE instead of double-casts scattered through the tests
* (#2522 review).
*/
function makeStoreEntry(filePath: string, overrides: Record<string, unknown>): ParsedFile {
return {
...(makeParsedFile(filePath) as unknown as Record<string, unknown>),
...overrides,
} as unknown as ParsedFile;
}
describe('parsedfile-store', () => {
it('round-trips ParsedFiles (incl. Scope Maps) and filters by requested paths', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [makeParsedFile('a.c'), makeParsedFile('b.c')]);
await persistParsedFileChunk(dir, 'chunk-1', [makeParsedFile('c.c')]);
// Filtering: only requested paths come back.
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c', 'c.c']));
expect([...loaded.keys()].sort()).toEqual(['a.c', 'c.c']);
expect(loaded.has('b.c')).toBe(false);
// Map fidelity: bindings / typeBindings survive as real Maps.
const a = loaded.get('a.c')!;
const scope = a.scopes[0];
expect(scope.bindings).toBeInstanceOf(Map);
expect(scope.bindings.get('fn')?.[0]?.defId).toBe('Function:a.c:fn');
expect(scope.typeBindings).toBeInstanceOf(Map);
expect((scope.typeBindings.get('x') as { name: string }).name).toBe('int');
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('repeated loads with different wantPaths each see their own files (shard-listing memo)', async () => {
// Scope resolution calls loadParsedFilesForPaths once per LANGUAGE over the
// same store, so the second and later passes reuse the shard path listings
// the first pass authenticated instead of re-reading every shard. The
// failure mode that memo introduces is a FALSE SKIP: pass 2 concludes a
// shard holds nothing it wants, and those files silently never reach the
// graph — an exit-0 wrong answer, not a crash. Each pass below wants files
// the previous pass did not, so a listing carried over from the wrong shard
// shows up as a missing file here.
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [makeParsedFile('a.c'), makeParsedFile('b.c')]);
await persistParsedFileChunk(dir, 'chunk-1', [makeParsedFile('c.c')]);
await persistParsedFileChunk(dir, 'chunk-2', [makeParsedFile('d.c')]);
const first = await loadParsedFilesForPaths(dir, new Set(['a.c']));
expect([...first.keys()]).toEqual(['a.c']);
// Key-set asserts alone still pass if the memo never skipped: the
// envelope listing would open the shard and skip deserialize. Spy
// open+deserialize on a later miss so a no-memo path fails.
const deserialize = vi.spyOn(v8, 'deserialize');
const open = vi.spyOn(nodeFsPromises, 'open');
try {
const second = await loadParsedFilesForPaths(dir, new Set(['c.c', 'd.c']));
expect([...second.keys()].sort()).toEqual(['c.c', 'd.c']);
expect(open.mock.calls.map(([file]) => path.basename(String(file))).sort()).toEqual([
'chunk-1.v8',
'chunk-2.v8',
]);
expect(deserialize).toHaveBeenCalledTimes(2);
open.mockClear();
deserialize.mockClear();
const third = await loadParsedFilesForPaths(dir, new Set(['b.c']));
expect([...third.keys()]).toEqual(['b.c']);
expect(open.mock.calls.map(([file]) => path.basename(String(file)))).toEqual([
'chunk-0.v8',
]);
expect(deserialize).toHaveBeenCalledTimes(1);
const fourth = await loadParsedFilesForPaths(dir, new Set(['a.c', 'b.c', 'c.c', 'd.c']));
expect([...fourth.keys()].sort()).toEqual(['a.c', 'b.c', 'c.c', 'd.c']);
} finally {
deserialize.mockRestore();
open.mockRestore();
}
// A shard written AFTER the memo was populated is still found: the memo
// holds listings, not the shard roster, and the roster is re-read per call.
await persistParsedFileChunk(dir, 'chunk-3', [makeParsedFile('e.c')]);
const fifth = await loadParsedFilesForPaths(dir, new Set(['e.c', 'a.c']));
expect([...fifth.keys()].sort()).toEqual(['a.c', 'e.c']);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('writes no shard for an empty chunk', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
await persistParsedFileChunk(dir, 'chunk-empty', []);
let shardCount = 0;
try {
shardCount = (await readdir(getParsedFileStoreDir(dir))).length;
} catch {
shardCount = 0; // dir not created — also fine
}
expect(shardCount).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('clearParsedFileStore removes all shards (subsequent load is empty)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [makeParsedFile('a.c')]);
await clearParsedFileStore(dir);
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c']));
expect(loaded.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns empty map when the store is absent', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c']));
expect(loaded.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('round-trips validated callable-flow operand and signature metadata', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const pf = makeStoreEntry('flow.cpp', {
callableFlowSites: [
{
kind: 'seed',
destination: {
name: 'member',
inScope: 'scope:entry',
atRange: { startLine: 3, startCol: 2, endLine: 3, endCol: 8 },
indirection: 0,
addressOf: false,
expressionKind: 'binding',
},
targetName: 'run',
targetQualifiedName: 'Base.run',
targetRange: { startLine: 3, startCol: 12, endLine: 3, endCol: 21 },
expectedSignature: {
parameterCount: 1,
parameterTypes: ['int'],
isConst: true,
},
},
],
});
await persistParsedFileChunk(dir, 'flow', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['flow.cpp']));
expect(loaded.get('flow.cpp')?.callableFlowSites).toEqual(pf.callableFlowSites);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('round-trips exact call-result assignment identities', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const pf = makeStoreEntry('Scenario.swift', {
callResultAssignmentSites: [
{
callSite: { startLine: 3, startCol: 14, endLine: 3, endCol: 25 },
inScope: 'scope:run',
lhs: 'store',
},
],
});
await persistParsedFileChunk(dir, 'assignment', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['Scenario.swift']));
expect(loaded.get('Scenario.swift')?.callResultAssignmentSites).toEqual(
pf.callResultAssignmentSites,
);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('drops malformed call-result assignments per site and rejects a non-array field', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const mixed = makeStoreEntry('mixed.swift', {
callResultAssignmentSites: [
{
callSite: { startLine: 3, startCol: 14, endLine: 3, endCol: 25 },
inScope: 'scope:run',
lhs: 'store',
},
{ callSite: '3:14', inScope: 'scope:run', lhs: 'poison' },
],
});
const garbage = makeStoreEntry('garbage.swift', {
callResultAssignmentSites: 'not-an-array',
});
await persistParsedFileChunk(dir, 'assignment-invalid', [mixed, garbage]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['mixed.swift', 'garbage.swift']));
expect(loaded.get('mixed.swift')?.callResultAssignmentSites).toHaveLength(1);
expect(loaded.has('garbage.swift')).toBe(false);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('drops a malformed callable-flow site but retains the file and its other sites (per-site sanitation, #2522)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const operand = {
name: 'callback',
inScope: 'scope:entry',
atRange: { startLine: 2, startCol: 2, endLine: 2, endCol: 10 },
indirection: 17,
addressOf: false,
expressionKind: 'binding',
};
const invalid = makeStoreEntry('invalid.c', {
callableFlowSites: [
{
kind: 'invoke',
callSite: { startLine: 2, startCol: 2, endLine: 2, endCol: 12 },
inScope: 'scope:entry',
callee: operand,
invocationKind: 'indirect',
arity: 0,
},
],
});
await persistParsedFileChunk(dir, 'invalid', [invalid, makeParsedFile('valid.c')]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['invalid.c', 'valid.c']));
// The file survives with the offending site dropped — a per-file
// rejection here caused a permanent, silent warm-cache reparse loop.
expect(loaded.get('invalid.c')?.callableFlowSites).toEqual([]);
expect(loaded.has('valid.c')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('accepts empty-string parameterTypes entries ("" = unknown type, real C++ extractor output)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const pf = makeStoreEntry('cv.cpp', {
callableFlowSites: [
{
kind: 'seed',
destination: {
name: 'fp',
inScope: 'scope:entry',
atRange: { startLine: 1, startCol: 0, endLine: 1, endCol: 8 },
indirection: 0,
addressOf: false,
expressionKind: 'binding',
},
targetName: 'handler',
targetRange: { startLine: 1, startCol: 12, endLine: 1, endCol: 19 },
expectedSignature: { parameterCount: 2, parameterTypes: ['int', ''] },
},
],
});
await persistParsedFileChunk(dir, 'cv', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['cv.cpp']));
expect(loaded.get('cv.cpp')?.callableFlowSites).toEqual(pf.callableFlowSites);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('rejects the whole file only when callableFlowSites is non-array garbage', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const garbage = makeStoreEntry('garbage.c', {
callableFlowSites: 'not-an-array',
});
await persistParsedFileChunk(dir, 'garbage', [garbage, makeParsedFile('ok.c')]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['garbage.c', 'ok.c']));
expect(loaded.has('garbage.c')).toBe(false);
expect(loaded.has('ok.c')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// #1983 parallel serialization: the sync worker writer and the async writer
// share one serialization core and MUST produce byte-identical shards (the
// loader's deep-equals masks byte drift, so assert raw bytes).
it('persistParsedFileShardSync writes byte-identical shards to the async writer', async () => {
const asyncDir = await mkdtemp(path.join(tmpdir(), 'pfstore-a-'));
const syncDir = await mkdtemp(path.join(tmpdir(), 'pfstore-s-'));
try {
const files = [makeParsedFile('a.c'), makeParsedFile('b.c')];
await persistParsedFileChunk(asyncDir, 'shard', files);
persistParsedFileShardSync(syncDir, 'shard', files);
const asyncBytes = await readFile(path.join(getParsedFileStoreDir(asyncDir), 'shard.v8'));
const syncBytes = await readFile(path.join(getParsedFileStoreDir(syncDir), 'shard.v8'));
expect(syncBytes.equals(asyncBytes)).toBe(true);
} finally {
await rm(asyncDir, { recursive: true, force: true });
await rm(syncDir, { recursive: true, force: true });
}
});
it('persistParsedFileShardSync round-trips through loadParsedFilesForPaths with Maps intact', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
persistParsedFileShardSync(dir, 'w1-0', [makeParsedFile('a.c')]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c']));
const scope = loaded.get('a.c')!.scopes[0];
expect(scope.bindings).toBeInstanceOf(Map);
expect(scope.bindings.get('fn')?.[0]?.defId).toBe('Function:a.c:fn');
expect(scope.typeBindings).toBeInstanceOf(Map);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// #1983 capture side-channel: a ParsedFile may carry a plain-data
// `captureSideChannel` (e.g. C++ ADL / namespace / two-phase marks the worker
// computed). It MUST survive the JSON store round-trip so the main thread can
// restore those module maps WITHOUT a re-parse. Plain objects/arrays only —
// no Maps/Sets — so the interning reviver passes them through unchanged.
it('round-trips a ParsedFile.captureSideChannel (plain data) through the store', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const sideChannel = {
adl: {
argInfoBySite: [
[
6,
4,
[
{
simpleClassName: 'Event',
templateSimpleClassName: '',
templateNamespace: '',
templateArgClassNames: [],
templateArgNamespaces: [],
},
],
],
],
noAdlSites: [[9, 2]],
},
inlineNamespaceRanges: ['1:0:3:1'],
fileLocal: {
fileLocalNames: ['helper'],
anonymousNamespaceRanges: ['4:0:6:1'],
},
twoPhase: {
dependentBases: [['Derived', [['Base', ['detail']]]]],
dependentPackBaseClasses: ['Mix'],
},
};
const pf = makeStoreEntry('app.cpp', {
captureSideChannel: sideChannel,
});
persistParsedFileShardSync(dir, 'w1-0', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['app.cpp']));
const got = loaded.get('app.cpp')!;
// Deep-equal: the plain-data snapshot survives byte-for-byte (after JSON).
expect((got as { captureSideChannel?: unknown }).captureSideChannel).toEqual(sideChannel);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// #1983 (Kotlin): the kotlin provider carries a self-describing capture
// side-channel containing companion scopes and class annotation facts. It
// shares the single generic `captureSideChannel` field with C++, so confirm
// the (Set→array) plain-data shape survives the JSON store round-trip too.
it('round-trips a Kotlin ParsedFile.captureSideChannel through the store', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const sideChannel = {
kind: 'kotlin',
companionScopes: ['scope:Logger.companion', 'scope:Animal.companion'],
packageFact: { status: 'known', packageName: 'com.example' },
classAnnotations: [
{
classScopeId: 'scope:App.kt#1:0-2:0:Class',
annotationNames: ['Service'],
},
],
};
const pf = makeStoreEntry('App.kt', {
captureSideChannel: sideChannel,
});
persistParsedFileShardSync(dir, 'w1-0', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['App.kt']));
const got = loaded.get('App.kt')!;
expect((got as { captureSideChannel?: unknown }).captureSideChannel).toEqual(sideChannel);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// #1983 (C): the C provider carries a self-describing static-linkage side-
// channel `{ kind: 'c', staticNames: string[] }` (the file-local `static`
// function names the worker recorded). It shares the single generic
// `captureSideChannel` field with C++/Kotlin, so confirm the plain-data shape
// survives the JSON store round-trip too — without it, `static` functions
// leak into cross-file resolution on the worker-only parse path.
it('round-trips a C ParsedFile.captureSideChannel through the store', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const sideChannel = { kind: 'c', staticNames: ['compute', 'helper'] };
const pf = makeStoreEntry('local.c', {
captureSideChannel: sideChannel,
});
persistParsedFileShardSync(dir, 'w1-0', [pf]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['local.c']));
const got = loaded.get('local.c')!;
expect((got as { captureSideChannel?: unknown }).captureSideChannel).toEqual(sideChannel);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('persistParsedFileShardSync writes no shard and no directory for empty input', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
persistParsedFileShardSync(dir, 'w1-0', []);
let entries: string[] = [];
try {
entries = await readdir(getParsedFileStoreDir(dir));
} catch {
entries = []; // store dir not created — the expected parity with the async writer
}
expect(entries).toHaveLength(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// Def-object dedup: each SymbolDefinition is serialized THREE times — in
// ParsedFile.localDefs, in the owning scope.ownedDefs, and inside
// scope.bindings[].def (BindingRef) — but is ONE object by reference in the
// live extractor. JSON.parse rebuilds three distinct objects; the load reviver
// must re-share them by nodeId (collapsing ~3× the def-object heap on the
// disk-backed/kernel path). Re-sharing is byte-identical to resolution because
// every consumer reads defs by value (nodeId/type), never by object identity.
it("re-shares a def's three serialized copies into one object on load", async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const def = {
nodeId: 'Function:a.c:fn',
filePath: 'a.c',
type: 'Function',
qualifiedName: 'fn',
isSynthetic: true,
};
const pf = {
filePath: 'a.c',
moduleScope: 'a.c:module',
parsedImports: [],
localDefs: [def], // copy 1
referenceSites: [],
scopes: [
{
id: 'a.c:module',
parent: null,
kind: 'Module',
range: { startLine: 1, startCol: 0, endLine: 9, endCol: 0 },
filePath: 'a.c',
bindings: new Map([['fn', [{ def }]]]), // copy 3 (BindingRef.def)
ownedDefs: [def], // copy 2
imports: [],
typeBindings: new Map(),
},
],
} as unknown as ParsedFile;
persistParsedFileShardSync(dir, 'w1-0', [pf]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['a.c']))).get('a.c')!;
const fromLocal = loaded.localDefs[0];
const scope = loaded.scopes[0];
const fromOwned = scope.ownedDefs[0];
const fromBinding = scope.bindings.get('fn')![0].def;
// All three deserialized copies are re-shared into ONE object.
expect(fromLocal).toBe(fromOwned);
expect(fromLocal).toBe(fromBinding);
// Value-identical to what was written.
expect(fromLocal).toEqual({
nodeId: 'Function:a.c:fn',
filePath: 'a.c',
type: 'Function',
qualifiedName: 'fn',
isSynthetic: true,
});
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('keeps defs with distinct nodeIds as distinct objects (no over-collapsing)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-'));
try {
const def1 = {
nodeId: 'Function:a.c:fn1',
filePath: 'a.c',
type: 'Function',
qualifiedName: 'fn1',
};
const def2 = {
nodeId: 'Function:a.c:fn2',
filePath: 'a.c',
type: 'Function',
qualifiedName: 'fn2',
};
const pf = {
filePath: 'a.c',
moduleScope: 'a.c:module',
parsedImports: [],
localDefs: [def1, def2],
referenceSites: [],
scopes: [
{
id: 'a.c:module',
parent: null,
kind: 'Module',
range: { startLine: 1, startCol: 0, endLine: 9, endCol: 0 },
filePath: 'a.c',
bindings: new Map([
['fn1', [{ def: def1 }]],
['fn2', [{ def: def2 }]],
]),
ownedDefs: [def1, def2],
imports: [],
typeBindings: new Map(),
},
],
} as unknown as ParsedFile;
persistParsedFileShardSync(dir, 'w1-0', [pf]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['a.c']))).get('a.c')!;
expect(loaded.localDefs[0]).not.toBe(loaded.localDefs[1]);
expect(loaded.localDefs[0].nodeId).toBe('Function:a.c:fn1');
expect(loaded.localDefs[1].nodeId).toBe('Function:a.c:fn2');
// Each still re-shares with its own ownedDefs copy.
expect(loaded.localDefs[0]).toBe(loaded.scopes[0].ownedDefs[0]);
expect(loaded.localDefs[1]).toBe(loaded.scopes[0].ownedDefs[1]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});
/**
* `receiverChain` at the untrusted boundary. Unlike `callableFlowSites`,
* `referenceSites` had no sanitizer here at all, so this field arrives with the
* first one.
*/
describe('parsedfile-store receiverChain sanitation', () => {
const siteWith = (receiverChain: unknown) => ({
name: 'save',
atRange: { startLine: 3, startCol: 2, endLine: 3, endCol: 6 },
inScope: 'x.ts:module',
kind: 'call',
...(receiverChain === undefined ? {} : { receiverChain }),
});
it('round-trips a well-formed chain', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [
makeStoreEntry('x.ts', { referenceSites: [siteWith('2|svc|cgetUser')] }),
]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['x.ts']))).get('x.ts')!;
expect(loaded.referenceSites[0]).toMatchObject({
name: 'save',
receiverChain: '2|svc|cgetUser',
});
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('loads a shard written before the field existed, unchanged', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-old-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [
makeStoreEntry('x.ts', { referenceSites: [siteWith(undefined)] }),
]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['x.ts']))).get('x.ts')!;
expect(loaded.referenceSites[0]).toMatchObject({ name: 'save' });
expect(loaded.referenceSites[0]).not.toHaveProperty('receiverChain');
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it.each([
['malformed', 'not-a-chain'],
['unknown future version', '3|svc|cgetUser'],
['superseded v1 payload', '1|svc|cgetUser'],
['over depth', '2|svc|ca|cb|cc|cd'],
['non-string', 42],
])(
'strips a %s chain but KEEPS the site — it still resolves via the text cascade',
async (_label, payload) => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-bad-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [
makeStoreEntry('x.ts', { referenceSites: [siteWith(payload)] }),
]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['x.ts']))).get('x.ts')!;
expect(loaded.referenceSites).toHaveLength(1);
expect(loaded.referenceSites[0]).toMatchObject({ name: 'save' });
expect(loaded.referenceSites[0]).not.toHaveProperty('receiverChain');
} finally {
await rm(dir, { recursive: true, force: true });
}
},
);
it('rejects the file when referenceSites is not an array at all', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-garbage-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [
makeStoreEntry('garbage.ts', { referenceSites: 'nonsense' }),
makeStoreEntry('ok.ts', { referenceSites: [siteWith('2|svc|cgetUser')] }),
]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['garbage.ts', 'ok.ts']));
expect(loaded.has('garbage.ts')).toBe(false);
expect(loaded.has('ok.ts')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('strips only the invalid chain and leaves a valid sibling intact', async () => {
// Sanitation is per-FIELD, not per-site or per-file. Every other case here
// uses a single-element array, so the `dropped > 0` .map() branch was never
// shown to preserve a good neighbour.
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-chain-mixed-'));
try {
await persistParsedFileChunk(dir, 'chunk-0', [
makeStoreEntry('x.ts', {
referenceSites: [
siteWith('2|svc|cgetUser'),
siteWith('not-a-chain'),
siteWith('2|other|ffield'),
],
}),
]);
const loaded = (await loadParsedFilesForPaths(dir, new Set(['x.ts']))).get('x.ts')!;
expect(loaded.referenceSites).toHaveLength(3);
expect(loaded.referenceSites[0]).toMatchObject({ receiverChain: '2|svc|cgetUser' });
expect(loaded.referenceSites[1]).not.toHaveProperty('receiverChain');
expect(loaded.referenceSites[2]).toMatchObject({ receiverChain: '2|other|ffield' });
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('writes one .v8 shard per chunk and skips deserialize for non-intersecting listings (#3087)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-v8-skip-'));
const deserialize = vi.spyOn(v8, 'deserialize');
try {
await persistParsedFileChunk(dir, 'chunk-0', [makeParsedFile('a.c')]);
await persistParsedFileChunk(dir, 'chunk-1', [makeParsedFile('b.c')]);
expect((await readdir(getParsedFileStoreDir(dir))).sort()).toEqual([
'chunk-0.v8',
'chunk-1.v8',
]);
deserialize.mockClear();
const loaded = await loadParsedFilesForPaths(dir, new Set(['b.c']));
expect([...loaded.keys()]).toEqual(['b.c']);
expect(deserialize).toHaveBeenCalledTimes(1);
} finally {
deserialize.mockRestore();
await rm(dir, { recursive: true, force: true });
}
});
it('misses when the embedded path listing is corrupted', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-listing-fb-'));
try {
await persistParsedFileChunk(dir, 'ok', [makeParsedFile('a.c')]);
const dest = path.join(getParsedFileStoreDir(dir), 'ok.v8');
const buf = await readFile(dest);
const v8len = buf.readUInt16LE(14);
const pathsOff = 16 + v8len + 12;
buf[pathsOff] = 0;
await writeFile(dest, buf);
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c']));
expect(loaded.has('a.c')).toBe(false);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('omits a path listing when a filePath contains a newline and still loads', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-listing-nl-'));
const weird = 'weird\nname.c';
const deserialize = vi.spyOn(v8, 'deserialize');
try {
await persistParsedFileChunk(dir, 'ok', [makeParsedFile(weird)]);
expect(await readdir(getParsedFileStoreDir(dir))).toEqual(['ok.v8']);
deserialize.mockClear();
const loaded = await loadParsedFilesForPaths(dir, new Set(['unrelated.c']));
expect(loaded.size).toBe(0);
expect(deserialize).toHaveBeenCalledTimes(1);
expect((await loadParsedFilesForPaths(dir, new Set([weird]))).has(weird)).toBe(true);
} finally {
deserialize.mockRestore();
await rm(dir, { recursive: true, force: true });
}
});
it('rewrites a shard in place when the path listing is no longer safe', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-listing-rewrite-'));
const weird = 'weird\nname.c';
try {
await persistParsedFileChunk(dir, 'ok', [makeParsedFile('safe.c')]);
await persistParsedFileChunk(dir, 'ok', [makeParsedFile(weird)]);
expect(await readdir(getParsedFileStoreDir(dir))).toEqual(['ok.v8']);
const loaded = await loadParsedFilesForPaths(dir, new Set([weird, 'safe.c']));
expect(loaded.has(weird)).toBe(true);
expect(loaded.has('safe.c')).toBe(false);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('does not forceGc on a small store (byte budget, not every 8 shards) (#3086)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-gc-'));
const gc = vi.fn();
const prev = parsedFileLoadGc.run;
parsedFileLoadGc.run = gc;
try {
for (let i = 0; i < 16; i++) {
await persistParsedFileChunk(dir, `s${i}`, [makeParsedFile(`f${i}.c`)]);
}
await loadParsedFilesForPaths(dir, new Set(Array.from({ length: 16 }, (_, i) => `f${i}.c`)));
expect(gc).not.toHaveBeenCalled();
} finally {
parsedFileLoadGc.run = prev;
await rm(dir, { recursive: true, force: true });
}
});
it('forceGc when accumulated raw JSON bytes reach parsedFileLoadGc.byteBudget (#3086)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-gc-pos-'));
const gc = vi.fn();
const prevRun = parsedFileLoadGc.run;
const prevBudget = parsedFileLoadGc.byteBudget;
parsedFileLoadGc.run = gc;
parsedFileLoadGc.byteBudget = 8;
try {
await persistParsedFileChunk(dir, 's0', [makeParsedFile('f0.c')]);
await loadParsedFilesForPaths(dir, new Set(['f0.c']));
expect(gc).toHaveBeenCalled();
} finally {
parsedFileLoadGc.run = prevRun;
parsedFileLoadGc.byteBudget = prevBudget;
await rm(dir, { recursive: true, force: true });
}
});
it('restores a complete durable chunk into a stable run-store snapshot', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-durable-load-'));
try {
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, 'abc', 1, 0, [makeParsedFile('a.c')]);
expect(await durableChunkHasShards(dir, 'abc', new Set(['a.c']))).toBe(true);
await rm(path.join(durable, 'abc'), { recursive: true, force: true });
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c']));
expect(loaded.has('a.c')).toBe(true);
expect(await readdir(getParsedFileStoreDir(dir))).toEqual(['abc-w1-0.v8']);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('rejects a durable chunk with corrupt or incomplete shard coverage', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-durable-partial-'));
try {
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, 'abc', 1, 0, [makeParsedFile('a.c')]);
persistDurableParsedFileShardSync(durable, 'abc', 2, 0, [makeParsedFile('b.c')]);
await writeFile(path.join(durable, 'abc', 'abc-w2-0.v8'), Buffer.from([0, 1, 2]));
expect(await durableChunkHasShards(dir, 'abc', new Set(['a.c', 'b.c']))).toBe(false);
expect(await readdir(getParsedFileStoreDir(dir))).toEqual([]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('rejects valid durable shards that do not cover every indexed path', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-durable-missing-'));
try {
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, 'abc', 1, 0, [makeParsedFile('a.c')]);
expect(await durableChunkHasShards(dir, 'abc', new Set(['a.c', 'b.c']))).toBe(false);
expect(await readdir(getParsedFileStoreDir(dir))).toEqual([]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('run-store shards overlay durable hits for the same path', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-overlay-'));
try {
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, 'abc', 1, 0, [makeParsedFile('a.c')]);
expect(await durableChunkHasShards(dir, 'abc', new Set(['a.c']))).toBe(true);
persistParsedFileShardSync(dir, 'w1-0', [makeParsedFile('other.c')]);
persistParsedFileShardSync(dir, 'w1-1', [makeStoreEntry('a.c', { moduleScope: 'from-run' })]);
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c', 'other.c']));
expect(loaded.get('a.c')?.moduleScope).toBe('from-run');
expect(loaded.has('other.c')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('clearParsedFileStore leaves the durable cache intact', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-durable-keep-'));
try {
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, 'abc', 1, 0, [makeParsedFile('a.c')]);
const src = path.join(durable, 'abc', 'abc-w1-0.v8');
const before = await readFile(src);
persistParsedFileShardSync(dir, 'w1-0', [makeParsedFile('run.c')]);
await clearParsedFileStore(dir);
expect(await readFile(src)).toEqual(before);
expect(await durableChunkHasShards(dir, 'abc', new Set(['a.c']))).toBe(true);
expect((await loadParsedFilesForPaths(dir, new Set(['a.c']))).has('a.c')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('round-trips Maps and shared def identity through V8 (#3089)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-v8-id-'));
try {
const def = {
nodeId: 'Function:a.c:fn',
filePath: 'a.c',
type: 'Function' as const,
qualifiedName: 'fn',
};
const pf = makeParsedFile('a.c');
(pf.localDefs as unknown as object[])[0] = def;
(pf.scopes[0] as { ownedDefs: object[] }).ownedDefs = [def];
await persistParsedFileChunk(dir, 'ok', [pf]);
const loadedFile = (await loadParsedFilesForPaths(dir, new Set(['a.c']))).get('a.c');
expect(loadedFile).toBeDefined();
if (!loadedFile) return;
expect(loadedFile.scopes[0].bindings).toBeInstanceOf(Map);
expect(loadedFile.localDefs[0]).toBe(loadedFile.scopes[0].ownedDefs[0]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('treats a missing or corrupt V8 shard as a miss with no JSON fallback', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-v8-miss-'));
try {
await persistParsedFileChunk(dir, 'gone', [makeParsedFile('a.c')]);
await persistParsedFileChunk(dir, 'junk', [makeParsedFile('b.c')]);
const storeDir = getParsedFileStoreDir(dir);
await rm(path.join(storeDir, 'gone.v8'));
await writeFile(path.join(storeDir, 'junk.v8'), Buffer.from([0, 1, 2, 3, 4]));
const loaded = await loadParsedFilesForPaths(dir, new Set(['a.c', 'b.c']));
expect(loaded.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns false when the atomic V8 publish cannot replace the dest', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'pfstore-v8-blocked-'));
try {
const dest = path.join(getParsedFileStoreDir(dir), 'ok.v8');
await nodeFsPromises.mkdir(dest, { recursive: true });
await writeFile(path.join(dest, 'occupied'), 'x', 'utf-8');
expect(await persistParsedFileChunk(dir, 'ok', [makeParsedFile('a.c')])).toBe(false);
expect((await loadParsedFilesForPaths(dir, new Set(['a.c']))).size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
// chmod is the only lever that makes a real `fs.rm` reject here, and root
// ignores directory write permission while Windows treats the mode bits as a
// near no-op. Skipping is honest; a green vacuous run is not.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
'keeps pruning and still writes the index when one chunk directory cannot be removed',
async () => {
// #3204: the non-survivor delete targets the same directory whose reset
// may have failed, so the causes that break the reset (permissions, a
// locked file, a read-only mount) break this rm too. One undeletable
// directory must not cost every other chunk its index entry.
const dir = await mkdtemp(path.join(tmpdir(), 'pf-rm-fail-'));
const durableDir = getDurableParsedFileDir(dir);
const undeletable = '3'.repeat(64);
const keep = '4'.repeat(64);
try {
await prepareDurableParsedFileChunk(durableDir, undeletable);
persistDurableParsedFileShardSync(durableDir, undeletable, 1, 0, [
makeParsedFile('gone.c'),
]);
await prepareDurableParsedFileChunk(durableDir, keep);
persistDurableParsedFileShardSync(durableDir, keep, 1, 0, [makeParsedFile('keep.c')]);
// Clearing write permission on the chunk directory makes its shards
// un-unlinkable, so the recursive rm of that directory rejects while the
// store root stays writable for the index rewrite.
const doomed = path.join(durableDir, undeletable);
await nodeFsPromises.chmod(doomed, 0o555);
await expect(
pruneAndSaveDurableParsedFileStore(durableDir, 'v-test', new Set([keep])),
).resolves.toBeUndefined();
// Assert the premise: the directory SURVIVED, i.e. the rm really did
// reject. Without this the test passes wherever the delete succeeds and
// would stay green if the try/catch were reverted.
expect(existsSync(doomed)).toBe(true);
const index = await loadDurableParsedFileIndex(durableDir, 'v-test');
expect(index.has(keep)).toBe(true);
expect(index.has(undeletable)).toBe(false);
} finally {
await nodeFsPromises.chmod(path.join(durableDir, undeletable), 0o755).catch(() => {});
await rm(dir, { recursive: true, force: true });
}
},
);
it('overlays staged durable chunks onto the live store without dropping live-only keys', async () => {
const live = await mkdtemp(path.join(tmpdir(), 'pf-live-'));
const staged = await mkdtemp(path.join(tmpdir(), 'pf-stg-'));
try {
const liveOnly = '1'.repeat(64);
const rewritten = '2'.repeat(64);
await prepareDurableParsedFileChunk(getDurableParsedFileDir(live), liveOnly);
persistDurableParsedFileShardSync(getDurableParsedFileDir(live), liveOnly, 1, 0, [
makeParsedFile('keep.c'),
]);
await prepareDurableParsedFileChunk(getDurableParsedFileDir(live), rewritten);
persistDurableParsedFileShardSync(getDurableParsedFileDir(live), rewritten, 1, 0, [
makeParsedFile('old.c'),
]);
await pruneAndSaveDurableParsedFileStore(
getDurableParsedFileDir(live),
'v-test',
new Set([liveOnly, rewritten]),
);
await prepareDurableParsedFileChunk(getDurableParsedFileDir(staged), rewritten);
persistDurableParsedFileShardSync(getDurableParsedFileDir(staged), rewritten, 1, 0, [
makeParsedFile('new.c'),
]);
await mergeStagedDurableParsedFileStore(
live,
staged,
'v-test',
new Set([liveOnly, rewritten]),
);
expect(await durableChunkHasShards(live, liveOnly, new Set(['keep.c']))).toBe(true);
expect(await durableChunkHasShards(live, rewritten, new Set(['new.c']))).toBe(true);
expect(await durableChunkHasShards(live, rewritten, new Set(['old.c']))).toBe(false);
} finally {
await rm(live, { recursive: true, force: true });
await rm(staged, { recursive: true, force: true });
}
});
});