From 6c82d84a2e18ddd456a0a762a654f1088c947395 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Sun, 7 Jun 2026 05:20:27 +0530 Subject: [PATCH] fix(ingestion): prune exported block-local value symbols --- .../src/core/ingestion/local-symbol-pruner.ts | 3 +- .../local-symbol-pruner-pipeline.test.ts | 71 +++++++++++++++++++ .../test/unit/local-symbol-pruner.test.ts | 12 ++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 gitnexus/test/integration/local-symbol-pruner-pipeline.test.ts diff --git a/gitnexus/src/core/ingestion/local-symbol-pruner.ts b/gitnexus/src/core/ingestion/local-symbol-pruner.ts index 69378bdf7..b94253397 100644 --- a/gitnexus/src/core/ingestion/local-symbol-pruner.ts +++ b/gitnexus/src/core/ingestion/local-symbol-pruner.ts @@ -24,8 +24,7 @@ const emptyStats = (skippedByEnv: boolean): LocalSymbolPruneStats => ({ const isLocalValueCandidate = (node: GraphNode): boolean => { if (!LOCAL_VALUE_LABELS.has(node.label)) return false; - if (node.properties.scope !== 'block') return false; - return node.properties.isExported !== true; + return node.properties.scope === 'block'; }; const isFileDefinesEdgeToCandidate = ( diff --git a/gitnexus/test/integration/local-symbol-pruner-pipeline.test.ts b/gitnexus/test/integration/local-symbol-pruner-pipeline.test.ts new file mode 100644 index 000000000..d660b9c0f --- /dev/null +++ b/gitnexus/test/integration/local-symbol-pruner-pipeline.test.ts @@ -0,0 +1,71 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js'; + +const describeIfWorkerBuilt = distWorkerExists() ? describe : describe.skip; + +let tmpDirs: string[] = []; + +const makeRepo = (source: string): string => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-local-prune-')); + tmpDirs.push(dir); + fs.writeFileSync(path.join(dir, 'sample.ts'), source, 'utf-8'); + return dir; +}; + +const findNode = ( + result: Awaited>, + label: string, + name: string, +) => result.graph.nodes.find((node) => node.label === label && node.properties.name === name); + +afterEach(() => { + for (const dir of tmpDirs) fs.rmSync(dir, { recursive: true, force: true }); + tmpDirs = []; +}); + +describeIfWorkerBuilt('local value symbol pruning pipeline', () => { + it('prunes inert locals inside exported functions without losing resolved calls', async () => { + const repo = makeRepo(` +const MODULE_CONST = 1; +export const exportedHandler = () => MODULE_CONST; + +export function run() { + const boring = 1; + const handler = () => boring; + const client = new Client(); + client.send(); + return handler(); +} + +class Client { + send() {} +} +`); + + const result = await runPipelineFromRepo(repo, () => {}, { + skipGraphPhases: true, + workerPoolSize: 1, + workerUrlForTest: DIST_WORKER_URL, + }); + + expect(findNode(result, 'Const', 'boring')).toBeUndefined(); + expect(findNode(result, 'Const', 'client')).toBeUndefined(); + expect(findNode(result, 'Const', 'handler')).toBeUndefined(); + + expect(findNode(result, 'Const', 'MODULE_CONST')).toBeDefined(); + expect(findNode(result, 'Const', 'exportedHandler')).toBeDefined(); + expect(findNode(result, 'Function', 'handler')).toBeDefined(); + + const keepsResolvedClientCall = result.graph.relationships.some((rel) => { + if (rel.type !== 'CALLS') return false; + const source = result.graph.getNode(rel.sourceId); + const target = result.graph.getNode(rel.targetId); + return source?.properties.name === 'run' && target?.properties.name === 'send'; + }); + expect(keepsResolvedClientCall).toBe(true); + }); +}); diff --git a/gitnexus/test/unit/local-symbol-pruner.test.ts b/gitnexus/test/unit/local-symbol-pruner.test.ts index dbe6b34df..d1d6e5f88 100644 --- a/gitnexus/test/unit/local-symbol-pruner.test.ts +++ b/gitnexus/test/unit/local-symbol-pruner.test.ts @@ -106,6 +106,18 @@ describe('pruneLocalValueSymbols', () => { expect(graph.relationshipCount).toBe(2); }); + it('prunes block-scope value symbols even when parser metadata marks them exported', () => { + const graph = createKnowledgeGraph(); + graph.addNode(fileNode()); + graph.addNode(node('Const:tmp', 'Const', { scope: 'block', isExported: true })); + graph.addRelationship(rel('rel:def', 'file:src/app.ts', 'Const:tmp')); + + const stats = pruneLocalValueSymbols(graph); + + expect(stats.prunedNodes).toBe(1); + expect(graph.getNode('Const:tmp')).toBeUndefined(); + }); + it('keeps block-scope value symbols defined by explicit scope graph nodes', () => { const graph = createKnowledgeGraph(); graph.addNode(fileNode());