From 56e2e7f520d297979293737480ede2e41552488b Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Thu, 9 Apr 2026 02:12:27 +0530 Subject: [PATCH] feat(type-env): add flush() method to TypeEnvironment Adds flush(filePath, accumulator) to the TypeEnvironment interface and buildTypeEnv return object, draining all scoped bindings into a BindingAccumulator. Adds 4 unit tests covering file-scope, function-scope, empty env, and multi-file accumulation. Co-Authored-By: Claude Sonnet 4.6 --- gitnexus/src/core/ingestion/type-env.ts | 15 ++++++ gitnexus/test/unit/type-env.test.ts | 63 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index e5fbb8cd2..0d3575a5e 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -8,6 +8,7 @@ import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js'; import { SupportedLanguages } from 'gitnexus-shared'; import { TYPED_PARAMETER_TYPES } from './type-extractors/shared.js'; import { getProvider } from './languages/index.js'; +import type { BindingAccumulator, BindingEntry } from './binding-accumulator.js'; import type { ClassNameLookup, ReturnTypeLookup, @@ -73,6 +74,9 @@ export interface TypeEnvironment { * Populated when a variable has BOTH a declared base type AND a more specific * constructor type (e.g., `Animal a = new Dog()` → key maps to 'Dog'). */ readonly constructorTypeMap: ReadonlyMap; + /** Drain all scoped bindings into a BindingAccumulator. + * Called once per file at end of processing. Skips empty scopes. */ + flush(filePath: string, accumulator: BindingAccumulator): void; } /** @@ -1245,6 +1249,17 @@ export const buildTypeEnv = ( fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE, allScopes: () => env as ReadonlyMap>, constructorTypeMap, + flush(filePath: string, accumulator: BindingAccumulator): void { + const entries: BindingEntry[] = []; + for (const [scope, scopeMap] of env) { + for (const [varName, typeName] of scopeMap) { + entries.push({ scope, varName, typeName }); + } + } + if (entries.length > 0) { + accumulator.appendFile(filePath, entries); + } + }, }; }; diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index db46bb31b..603922b0e 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { buildTypeEnv, type TypeEnvironment } from '../../src/core/ingestion/type-env.js'; +import { BindingAccumulator } from '../../src/core/ingestion/binding-accumulator.js'; import { createSymbolTable, type SymbolDefinition, @@ -5859,4 +5860,66 @@ function process() { expect(typeEnv.lookup('c', validateCall)).toBe('Config'); }); }); + + describe('flush', () => { + it('flushes file-scope bindings into accumulator', () => { + const code = `const user: User = getUser();\nconst count: number = 0;`; + const tree = parse(code, TypeScript.typescript); + const typeEnv = buildTypeEnv(tree, 'typescript'); + const acc = new BindingAccumulator(); + + typeEnv.flush('/src/test.ts', acc); + + const entries = acc.getFile('/src/test.ts'); + expect(entries).toBeDefined(); + const userEntry = entries!.find(e => e.varName === 'user'); + expect(userEntry).toBeDefined(); + expect(userEntry!.typeName).toBe('User'); + expect(userEntry!.scope).toBe(''); + }); + + it('flushes function-scoped bindings into accumulator', () => { + const code = `function process() {\n const result: Response = fetch();\n}`; + const tree = parse(code, TypeScript.typescript); + const typeEnv = buildTypeEnv(tree, 'typescript'); + const acc = new BindingAccumulator(); + + typeEnv.flush('/src/test.ts', acc); + + const entries = acc.getFile('/src/test.ts'); + expect(entries).toBeDefined(); + const resultEntry = entries!.find(e => e.varName === 'result'); + expect(resultEntry).toBeDefined(); + expect(resultEntry!.typeName).toBe('Response'); + expect(resultEntry!.scope).not.toBe(''); + }); + + it('flushes nothing for an empty TypeEnv', () => { + const code = `// empty file`; + const tree = parse(code, TypeScript.typescript); + const typeEnv = buildTypeEnv(tree, 'typescript'); + const acc = new BindingAccumulator(); + + typeEnv.flush('/src/empty.ts', acc); + + expect(acc.getFile('/src/empty.ts')).toBeUndefined(); + }); + + it('multiple files flush into same accumulator', () => { + const code1 = `const a: A = makeA();`; + const code2 = `const b: B = makeB();`; + const tree1 = parse(code1, TypeScript.typescript); + const tree2 = parse(code2, TypeScript.typescript); + const typeEnv1 = buildTypeEnv(tree1, 'typescript'); + const typeEnv2 = buildTypeEnv(tree2, 'typescript'); + const acc = new BindingAccumulator(); + + typeEnv1.flush('/src/a.ts', acc); + typeEnv2.flush('/src/b.ts', acc); + + expect(acc.fileCount).toBe(2); + expect(acc.getFile('/src/a.ts')).toBeDefined(); + expect(acc.getFile('/src/b.ts')).toBeDefined(); + }); + }); });