From ed58a5f647c5aea36f36dc8a3a3541aa8edd955c Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:01:01 -0600 Subject: [PATCH 1/6] fix(types): narrow parser return types from Promise to ExtractorOutput MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parseFileAuto, parseFilesAuto, parseFileIncremental now return Promise and Promise> - patchNativeResult returns ExtractorOutput (normalizes typeMap array→Map) - WasmExtractResult.symbols narrowed from any to ExtractorOutput - WasmExtractResult.langId narrowed from string to LanguageId - backfillTypeMap returns typed Map - Add _typeMapBackfilled field to ExtractorOutput interface - Remove dead instanceof Map branches now that types are consistent Impact: 8 functions changed, 16 affected --- src/domain/parser.ts | 68 ++++++++++++++++++++------------------------ src/types.ts | 2 ++ 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index 026559ad..85084026 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -5,7 +5,13 @@ import type { Tree } from 'web-tree-sitter'; import { Language, Parser, Query } from 'web-tree-sitter'; import { debug, warn } from '../infrastructure/logger.js'; import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js'; -import type { EngineMode, LanguageRegistryEntry } from '../types.js'; +import type { + EngineMode, + ExtractorOutput, + LanguageId, + LanguageRegistryEntry, + TypeMapEntry, +} from '../types.js'; // Re-export all extractors for backward compatibility export { @@ -67,12 +73,10 @@ interface ResolvedEngine { native: any; } -// biome-ignore lint/suspicious/noExplicitAny: extractor return types vary per language interface WasmExtractResult { - // biome-ignore lint/suspicious/noExplicitAny: extractor return shapes vary per language - symbols: any; + symbols: ExtractorOutput; tree: Tree; - langId: string; + langId: LanguageId; } // Shared patterns for all JS/TS/TSX (class_declaration excluded — name type differs) @@ -271,7 +275,8 @@ function resolveEngine(opts: ParseEngineOpts = {}): ResolvedEngine { * - dataflow argFlows/mutations bindingType -> binding wrapper */ // biome-ignore lint/suspicious/noExplicitAny: native result has dynamic shape -function patchNativeResult(r: any): any { +// biome-ignore lint/suspicious/noExplicitAny: native addon result has no type declarations +function patchNativeResult(r: any): ExtractorOutput { // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count" r.lineCount = r.lineCount ?? r.line_count ?? null; r._lineCount = r.lineCount; @@ -299,6 +304,13 @@ function patchNativeResult(r: any): any { } } + // typeMap: native returns an array of {name, typeName}; normalize to Map + if (r.typeMap && !(r.typeMap instanceof Map)) { + r.typeMap = new Map( + r.typeMap.map((e: { name: string; typeName: string }) => [e.name, e.typeName]), + ); + } + // dataflow: wrap bindingType into binding object for argFlows and mutations if (r.dataflow) { if (r.dataflow.argFlows) { @@ -419,29 +431,22 @@ export const SUPPORTED_EXTENSIONS: Set = new Set(_extToLang.keys()); async function backfillTypeMap( filePath: string, source?: string, -): Promise<{ typeMap: any; backfilled: boolean }> { +): Promise<{ typeMap: Map; backfilled: boolean }> { let code = source; if (!code) { try { code = fs.readFileSync(filePath, 'utf-8'); } catch { - return { typeMap: [], backfilled: false }; + return { typeMap: new Map(), backfilled: false }; } } const parsers = await createParsers(); const extracted = wasmExtractSymbols(parsers, filePath, code); try { if (!extracted?.symbols?.typeMap) { - return { typeMap: [], backfilled: false }; + return { typeMap: new Map(), backfilled: false }; } - const tm = extracted.symbols.typeMap; - return { - typeMap: - tm instanceof Map - ? tm - : new Map(tm.map((e: { name: string; typeName: string }) => [e.name, e.typeName])), - backfilled: true, - }; + return { typeMap: extracted.symbols.typeMap, backfilled: true }; } finally { // Free the WASM tree to prevent memory accumulation across repeated builds if (extracted?.tree && typeof extracted.tree.delete === 'function') { @@ -485,12 +490,11 @@ function wasmExtractSymbols( /** * Parse a single file and return normalized symbols. */ -// biome-ignore lint/suspicious/noExplicitAny: return shape varies between native and WASM engines export async function parseFileAuto( filePath: string, source: string, opts: ParseEngineOpts = {}, -): Promise { +): Promise { const { native } = resolveEngine(opts); if (native) { @@ -500,7 +504,7 @@ export async function parseFileAuto( // Only backfill typeMap for TS/TSX — JS files have no type annotations, // and the native engine already handles `new Expr()` patterns. if ( - (!patched.typeMap || patched.typeMap.length === 0) && + (!patched.typeMap || patched.typeMap.size === 0) && TS_BACKFILL_EXTS.has(path.extname(filePath)) ) { const { typeMap, backfilled } = await backfillTypeMap(filePath, source); @@ -519,15 +523,13 @@ export async function parseFileAuto( /** * Parse multiple files in bulk and return a Map. */ -// biome-ignore lint/suspicious/noExplicitAny: return shape varies between native and WASM engines export async function parseFilesAuto( filePaths: string[], rootDir: string, opts: ParseEngineOpts = {}, -): Promise> { +): Promise> { const { native } = resolveEngine(opts); - // biome-ignore lint/suspicious/noExplicitAny: result values have dynamic shape from extractors - const result = new Map(); + const result = new Map(); if (native) { const nativeResults = native.parseFiles( @@ -542,7 +544,7 @@ export async function parseFilesAuto( const patched = patchNativeResult(r); const relPath = path.relative(rootDir, r.file).split(path.sep).join('/'); result.set(relPath, patched); - if (!patched.typeMap || patched.typeMap.length === 0) { + if (!patched.typeMap || patched.typeMap.size === 0) { needsTypeMap.push({ filePath: r.file, relPath }); } } @@ -563,15 +565,7 @@ export async function parseFilesAuto( if (extracted?.symbols?.typeMap) { const symbols = result.get(relPath); if (!symbols) continue; - symbols.typeMap = - extracted.symbols.typeMap instanceof Map - ? extracted.symbols.typeMap - : new Map( - extracted.symbols.typeMap.map((e: { name: string; typeName: string }) => [ - e.name, - e.typeName, - ]), - ); + symbols.typeMap = extracted.symbols.typeMap; symbols._typeMapBackfilled = true; } } catch { @@ -651,13 +645,13 @@ export function createParseTreeCache(): any { /** * Parse a file incrementally using the cache, or fall back to full parse. */ -// biome-ignore lint/suspicious/noExplicitAny: cache is native ParseTreeCache with no type declarations; return shape varies +// biome-ignore lint/suspicious/noExplicitAny: cache is native ParseTreeCache with no type declarations export async function parseFileIncremental( cache: any, filePath: string, source: string, opts: ParseEngineOpts = {}, -): Promise { +): Promise { if (cache) { const result = cache.parseFile(filePath, source); if (!result) return null; @@ -665,7 +659,7 @@ export async function parseFileIncremental( // Only backfill typeMap for TS/TSX — JS files have no type annotations, // and the native engine already handles `new Expr()` patterns. if ( - (!patched.typeMap || patched.typeMap.length === 0) && + (!patched.typeMap || patched.typeMap.size === 0) && TS_BACKFILL_EXTS.has(path.extname(filePath)) ) { const { typeMap, backfilled } = await backfillTypeMap(filePath, source); diff --git a/src/types.ts b/src/types.ts index 70b1fadb..2bc5b4a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -454,6 +454,8 @@ export interface ExtractorOutput { dataflow?: DataflowResult; /** AST node rows, populated post-analysis. */ astNodes?: ASTNodeRow[]; + /** Set when typeMap was backfilled from WASM for a native parse result. */ + _typeMapBackfilled?: boolean; } /** Extractor function signature. */ From d1e5256ded415d8a7b2d543cb0e6d934e901bc86 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:01:19 -0600 Subject: [PATCH 2/6] fix(types): use cachedStmt pattern in buildTestFileIds Apply StmtCache pattern to the two static SQL queries in buildTestFileIds (module-map.ts), consistent with the rest of the query layer. Impact: 1 functions changed, 6 affected --- src/domain/analysis/module-map.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/domain/analysis/module-map.ts b/src/domain/analysis/module-map.ts index 7e300ea5..e511aa63 100644 --- a/src/domain/analysis/module-map.ts +++ b/src/domain/analysis/module-map.ts @@ -1,10 +1,11 @@ import path from 'node:path'; import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js'; +import { cachedStmt } from '../../db/repository/cached-stmt.js'; import { loadConfig } from '../../infrastructure/config.js'; import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js'; -import type { BetterSqlite3Database } from '../../types.js'; +import type { BetterSqlite3Database, StmtCache } from '../../types.js'; import { findCycles } from '../graph/cycles.js'; import { LANGUAGE_REGISTRY } from '../parser.js'; @@ -44,11 +45,15 @@ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20; // Section helpers // --------------------------------------------------------------------------- +const _fileNodesStmt: StmtCache<{ id: number; file: string }> = new WeakMap(); +const _allNodesIdFileStmt: StmtCache<{ id: number; file: string }> = new WeakMap(); + function buildTestFileIds(db: BetterSqlite3Database): Set { - const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all() as Array<{ - id: number; - file: string; - }>; + const allFileNodes = cachedStmt( + _fileNodesStmt, + db, + "SELECT id, file FROM nodes WHERE kind = 'file'", + ).all(); const testFileIds = new Set(); const testFiles = new Set(); for (const n of allFileNodes) { @@ -57,10 +62,7 @@ function buildTestFileIds(db: BetterSqlite3Database): Set { testFiles.add(n.file); } } - const allNodes = db.prepare('SELECT id, file FROM nodes').all() as Array<{ - id: number; - file: string; - }>; + const allNodes = cachedStmt(_allNodesIdFileStmt, db, 'SELECT id, file FROM nodes').all(); for (const n of allNodes) { if (testFiles.has(n.file)) testFileIds.add(n.id); } From 92ce3c0e3b17380e54ff33d9bcddaece1d299976 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:10:44 -0600 Subject: [PATCH 3/6] fix(types): construct TypeMapEntry objects in patchNativeResult (#569) --- src/domain/parser.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index 85084026..57b3636d 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -274,7 +274,6 @@ function resolveEngine(opts: ParseEngineOpts = {}): ResolvedEngine { * - Backward compat for older native binaries missing js_name annotations * - dataflow argFlows/mutations bindingType -> binding wrapper */ -// biome-ignore lint/suspicious/noExplicitAny: native result has dynamic shape // biome-ignore lint/suspicious/noExplicitAny: native addon result has no type declarations function patchNativeResult(r: any): ExtractorOutput { // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count" @@ -307,7 +306,10 @@ function patchNativeResult(r: any): ExtractorOutput { // typeMap: native returns an array of {name, typeName}; normalize to Map if (r.typeMap && !(r.typeMap instanceof Map)) { r.typeMap = new Map( - r.typeMap.map((e: { name: string; typeName: string }) => [e.name, e.typeName]), + r.typeMap.map((e: { name: string; typeName: string }) => [ + e.name, + { type: e.typeName, confidence: 0.9 } as TypeMapEntry, + ]), ); } From 87aebfbdf47b0e6280f4e35e2c47e10c470ceb69 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:05:07 -0600 Subject: [PATCH 4/6] fix(types): default typeMap to empty Map for non-TS native results (#569) patchNativeResult now initializes typeMap to an empty Map when the native engine omits it (common for Python, Go, Rust, etc.), ensuring the returned object satisfies the ExtractorOutput contract and callers can safely access .entries()/.size without null checks. --- src/domain/parser.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index 57b3636d..e6510b83 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -303,8 +303,12 @@ function patchNativeResult(r: any): ExtractorOutput { } } - // typeMap: native returns an array of {name, typeName}; normalize to Map - if (r.typeMap && !(r.typeMap instanceof Map)) { + // typeMap: native returns an array of {name, typeName}; normalize to Map. + // Non-TS languages may omit typeMap entirely — default to empty Map so + // callers can safely access .entries()/.size without null checks. + if (!r.typeMap) { + r.typeMap = new Map(); + } else if (!(r.typeMap instanceof Map)) { r.typeMap = new Map( r.typeMap.map((e: { name: string; typeName: string }) => [ e.name, From 4cf1645dedeeda96a63cf07ef341b62eccdcfd8c Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:31:17 -0600 Subject: [PATCH 5/6] fix: remove dead !patched.typeMap guard after patchNativeResult (#569) patchNativeResult now always sets typeMap to a Map, so the falsy check is dead code. Simplified all three sites to just check .size === 0. --- src/domain/parser.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index e6510b83..bcd8dfe2 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -509,10 +509,7 @@ export async function parseFileAuto( const patched = patchNativeResult(result); // Only backfill typeMap for TS/TSX — JS files have no type annotations, // and the native engine already handles `new Expr()` patterns. - if ( - (!patched.typeMap || patched.typeMap.size === 0) && - TS_BACKFILL_EXTS.has(path.extname(filePath)) - ) { + if (patched.typeMap.size === 0 && TS_BACKFILL_EXTS.has(path.extname(filePath))) { const { typeMap, backfilled } = await backfillTypeMap(filePath, source); patched.typeMap = typeMap; if (backfilled) patched._typeMapBackfilled = true; @@ -550,7 +547,7 @@ export async function parseFilesAuto( const patched = patchNativeResult(r); const relPath = path.relative(rootDir, r.file).split(path.sep).join('/'); result.set(relPath, patched); - if (!patched.typeMap || patched.typeMap.size === 0) { + if (patched.typeMap.size === 0) { needsTypeMap.push({ filePath: r.file, relPath }); } } @@ -664,10 +661,7 @@ export async function parseFileIncremental( const patched = patchNativeResult(result); // Only backfill typeMap for TS/TSX — JS files have no type annotations, // and the native engine already handles `new Expr()` patterns. - if ( - (!patched.typeMap || patched.typeMap.size === 0) && - TS_BACKFILL_EXTS.has(path.extname(filePath)) - ) { + if (patched.typeMap.size === 0 && TS_BACKFILL_EXTS.has(path.extname(filePath))) { const { typeMap, backfilled } = await backfillTypeMap(filePath, source); patched.typeMap = typeMap; if (backfilled) patched._typeMapBackfilled = true; From b27e79a7f1d1f2ff2da7fd441fc7fc64d8ba8dfd Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:01:30 -0600 Subject: [PATCH 6/6] fix(types): use .size check instead of truthiness for typeMap backfill guard typeMap is always a Map (truthy even when empty), so the ?.typeMap guard was incorrectly setting _typeMapBackfilled for empty Maps. Check .size > 0 to match the same invariant fixed elsewhere. --- src/domain/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index f0ca1be3..ecbc3f88 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -565,7 +565,7 @@ export async function parseFilesAuto( try { const code = fs.readFileSync(filePath, 'utf-8'); extracted = wasmExtractSymbols(parsers, filePath, code); - if (extracted?.symbols?.typeMap) { + if (extracted?.symbols && extracted.symbols.typeMap.size > 0) { const symbols = result.get(relPath); if (!symbols) continue; symbols.typeMap = extracted.symbols.typeMap;