Skip to content
Merged
13 changes: 7 additions & 6 deletions src/domain/analysis/module-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
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<number>();
const testFiles = new Set<string>();
for (const n of allFileNodes) {
Expand All @@ -67,8 +69,7 @@ function buildTestFileIds(db: BetterSqlite3Database): Set<number> {
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);
}
Expand Down
83 changes: 38 additions & 45 deletions src/domain/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -417,32 +437,22 @@ export const SUPPORTED_EXTENSIONS: Set<string> = new Set(_extToLang.keys());
async function backfillTypeMap(
filePath: string,
source?: string,
): Promise<{ typeMap: any; backfilled: boolean }> {
): Promise<{ typeMap: Map<string, TypeMapEntry>; 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') {
Expand Down Expand Up @@ -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<any> {
): Promise<ExtractorOutput | null> {
const { native } = resolveEngine(opts);

if (native) {
Expand All @@ -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;
Expand All @@ -520,15 +526,13 @@ export async function parseFileAuto(
/**
* Parse multiple files in bulk and return a Map<relPath, symbols>.
*/
// biome-ignore lint/suspicious/noExplicitAny: return shape varies between native and WASM engines
export async function parseFilesAuto(
filePaths: string[],
rootDir: string,
opts: ParseEngineOpts = {},
): Promise<Map<string, any>> {
): Promise<Map<string, ExtractorOutput>> {
const { native } = resolveEngine(opts);
// biome-ignore lint/suspicious/noExplicitAny: result values have dynamic shape from extractors
const result = new Map<string, any>();
const result = new Map<string, ExtractorOutput>();

if (native) {
const nativeResults = native.parseFiles(
Expand All @@ -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 });
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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<any> {
): Promise<ExtractorOutput | null> {
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;
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading