diff --git a/src/domain/analysis/module-map.ts b/src/domain/analysis/module-map.ts index 918bc2dc..b2c3f8de 100644 --- a/src/domain/analysis/module-map.ts +++ b/src/domain/analysis/module-map.ts @@ -52,13 +52,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 fileNodesStmt = cachedStmt( - _fileNodesStmtCache, + const allFileNodes = cachedStmt( + _fileNodesStmt, db, "SELECT id, file FROM nodes WHERE kind = 'file'", - ); - const allFileNodes = fileNodesStmt.all() as Array<{ id: number; file: string }>; + ).all(); const testFileIds = new Set(); const testFiles = new Set(); for (const n of allFileNodes) { @@ -67,8 +69,7 @@ function buildTestFileIds(db: BetterSqlite3Database): Set { testFiles.add(n.file); } } - const allNodesStmt = cachedStmt(_allNodesStmtCache, db, 'SELECT id, file FROM nodes'); - const allNodes = allNodesStmt.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); } diff --git a/src/domain/parser.ts b/src/domain/parser.ts index 651719d8..ecbc3f88 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, ExtractorOutput, LanguageId, LanguageRegistryEntry } from '../types.js'; +import type { + EngineMode, + ExtractorOutput, + LanguageId, + LanguageRegistryEntry, + TypeMapEntry, +} from '../types.js'; // Re-export all extractors for backward compatibility export { @@ -268,8 +274,8 @@ 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 -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; @@ -297,6 +303,20 @@ function patchNativeResult(r: any): any { } } + // 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, + { type: e.typeName, confidence: 0.9 } as TypeMapEntry, + ]), + ); + } + // dataflow: wrap bindingType into binding object for argFlows and mutations if (r.dataflow) { if (r.dataflow.argFlows) { @@ -417,32 +437,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 }; + if (!extracted || extracted.symbols.typeMap.size === 0) { + return { typeMap: new Map(), backfilled: false }; } - const tm = extracted.symbols.typeMap; - return { - typeMap: - tm instanceof Map - ? tm - : new Map( - // biome-ignore lint/suspicious/noExplicitAny: defensive fallback for legacy array-shaped typeMap - (tm as any).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') { @@ -486,12 +496,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,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.length === 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; @@ -520,15 +526,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( @@ -543,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.length === 0) { + if (patched.typeMap.size === 0) { needsTypeMap.push({ filePath: r.file, relPath }); } } @@ -561,18 +565,10 @@ 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 instanceof Map - ? extracted.symbols.typeMap - : new Map( - // biome-ignore lint/suspicious/noExplicitAny: defensive fallback for legacy array-shaped typeMap - (extracted.symbols.typeMap as any).map( - (e: { name: string; typeName: string }) => [e.name, e.typeName], - ), - ); + symbols.typeMap = extracted.symbols.typeMap; symbols._typeMapBackfilled = true; } } catch { @@ -652,23 +648,20 @@ 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; 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.length === 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; diff --git a/src/types.ts b/src/types.ts index 04c59b4a..77eb612a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -464,6 +464,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. */