From 3b8e6123e46dbe798b744532639472c62a417fee Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:15:17 -0600 Subject: [PATCH 01/15] feat(types): add SQLite, StmtCache, and NativeAddon foundation types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add §21 (SqliteStatement, BetterSqlite3Database, StmtCache) and §22 (NativeAddon, NativeParseTreeCache) to types.ts. These interfaces capture the exact better-sqlite3 and napi-rs surfaces used by the repository and native modules, enabling type-safe migration without @types/better-sqlite3. Impact: 24 functions changed, 0 affected --- src/types.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/types.ts b/src/types.ts index 807a5e98..cdc5be0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1637,3 +1637,69 @@ export interface ExportNeo4jCSVResult { nodes: string; relationships: string; } + +// ════════════════════════════════════════════════════════════════════════ +// §21 SQLite Database (better-sqlite3 surface) +// ════════════════════════════════════════════════════════════════════════ + +/** Minimal prepared-statement interface matching better-sqlite3. */ +export interface SqliteStatement { + get(...params: unknown[]): TRow | undefined; + all(...params: unknown[]): TRow[]; + run(...params: unknown[]): { changes: number; lastInsertRowid: number | bigint }; + iterate(...params: unknown[]): IterableIterator; +} + +/** Minimal database interface matching the better-sqlite3 surface we use. */ +export interface BetterSqlite3Database { + prepare(sql: string): SqliteStatement; + exec(sql: string): void; + close(): void; + pragma(sql: string): unknown; + transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T; + readonly open: boolean; + readonly name: string; +} + +/** WeakMap-based statement cache: one prepared statement per db instance. */ +export type StmtCache = WeakMap>; + +// ════════════════════════════════════════════════════════════════════════ +// §22 Native Addon (napi-rs FFI boundary) +// ════════════════════════════════════════════════════════════════════════ + +/** The native napi-rs addon interface (crates/codegraph-core). */ +export interface NativeAddon { + parseFile(filePath: string, source: string, dataflow: boolean, ast: boolean): unknown; + parseFiles( + files: Array<{ filePath: string; source: string }>, + dataflow: boolean, + ast: boolean, + ): unknown[]; + resolveImport( + fromFile: string, + importSource: string, + rootDir: string, + extensions: string[], + aliases: unknown, + ): string | null; + resolveImports( + items: Array<{ fromFile: string; importSource: string }>, + rootDir: string, + extensions: string[], + aliases: unknown, + ): Array<{ key: string; resolved: string | null }>; + computeConfidence(callerFile: string, targetFile: string, importedFrom: string | null): number; + detectCycles(edges: Array<{ source: string; target: string }>): string[][]; + buildCallEdges(files: unknown[], nodes: unknown[], builtinReceivers: string[]): unknown[]; + engineVersion(): string; + ParseTreeCache: new () => NativeParseTreeCache; +} + +/** Native parse-tree cache instance. */ +export interface NativeParseTreeCache { + get(filePath: string): unknown; + set(filePath: string, tree: unknown): void; + delete(filePath: string): void; + clear(): void; +} From 2c3d27bf6c0599b343be6b160aa5aa55b5b69c04 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:01:59 -0600 Subject: [PATCH 02/15] feat(types): convert db/repository layer from JS to TypeScript Rename all 14 src/db/repository/*.js files to .ts and add type annotations throughout the repository module (base, nodes, edges, embeddings, complexity, cfg, cochange, dataflow, graph-read, build-stmts, cached-stmt, in-memory-repository, sqlite-repository, index barrel). Add Node.js ESM loader hook (scripts/ts-resolve-hooks.js) that falls back from .js to .ts imports when the .js file no longer exists on disk, enabling gradual migration where .js consumers still use .js import specifiers. Update vitest.config.js with a Vite resolve plugin and NODE_OPTIONS registration so require() calls and child processes in tests resolve correctly. --- scripts/ts-resolve-hooks.js | 29 +++ scripts/ts-resolve-loader.js | 15 ++ src/db/repository/base.js | 201 --------------- src/db/repository/base.ts | 190 ++++++++++++++ .../{build-stmts.js => build-stmts.ts} | 50 ++-- .../{cached-stmt.js => cached-stmt.ts} | 15 +- src/db/repository/{cfg.js => cfg.ts} | 45 ++-- .../repository/{cochange.js => cochange.ts} | 22 +- .../{complexity.js => complexity.ts} | 11 +- src/db/repository/dataflow.js | 17 -- src/db/repository/dataflow.ts | 19 ++ src/db/repository/{edges.js => edges.ts} | 152 ++++------- .../{embeddings.js => embeddings.ts} | 24 +- .../{graph-read.js => graph-read.ts} | 34 +-- ...-repository.js => in-memory-repository.ts} | 243 +++++++++++------- src/db/repository/{index.js => index.ts} | 0 src/db/repository/{nodes.js => nodes.ts} | 182 ++++++------- ...ite-repository.js => sqlite-repository.ts} | 112 +++++--- vitest.config.js | 39 +++ 19 files changed, 755 insertions(+), 645 deletions(-) create mode 100644 scripts/ts-resolve-hooks.js create mode 100644 scripts/ts-resolve-loader.js delete mode 100644 src/db/repository/base.js create mode 100644 src/db/repository/base.ts rename src/db/repository/{build-stmts.js => build-stmts.ts} (72%) rename src/db/repository/{cached-stmt.js => cached-stmt.ts} (55%) rename src/db/repository/{cfg.js => cfg.ts} (59%) rename src/db/repository/{cochange.js => cochange.ts} (60%) rename src/db/repository/{complexity.js => complexity.ts} (62%) delete mode 100644 src/db/repository/dataflow.js create mode 100644 src/db/repository/dataflow.ts rename src/db/repository/{edges.js => edges.ts} (61%) rename src/db/repository/{embeddings.js => embeddings.ts} (52%) rename src/db/repository/{graph-read.js => graph-read.ts} (54%) rename src/db/repository/{in-memory-repository.js => in-memory-repository.ts} (72%) rename src/db/repository/{index.js => index.ts} (100%) rename src/db/repository/{nodes.js => nodes.ts} (55%) rename src/db/repository/{sqlite-repository.js => sqlite-repository.ts} (59%) diff --git a/scripts/ts-resolve-hooks.js b/scripts/ts-resolve-hooks.js new file mode 100644 index 00000000..8da894c5 --- /dev/null +++ b/scripts/ts-resolve-hooks.js @@ -0,0 +1,29 @@ +/** + * ESM resolve/load hooks for .js → .ts fallback during gradual migration. + * + * - resolve: when a .js specifier resolves to a path that doesn't exist, + * check if a .ts version exists and redirect to it. + * - load: for .ts files, strip type annotations using Node 22's built-in + * --experimental-strip-types via a source transform. + */ + +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +export async function resolve(specifier, context, nextResolve) { + try { + return await nextResolve(specifier, context); + } catch (err) { + // Only intercept ERR_MODULE_NOT_FOUND for .js specifiers + if (err.code === 'ERR_MODULE_NOT_FOUND' && specifier.endsWith('.js')) { + const tsSpecifier = specifier.replace(/\.js$/, '.ts'); + try { + return await nextResolve(tsSpecifier, context); + } catch { + // .ts also not found — throw the original error + } + } + throw err; + } +} diff --git a/scripts/ts-resolve-loader.js b/scripts/ts-resolve-loader.js new file mode 100644 index 00000000..aa7f4531 --- /dev/null +++ b/scripts/ts-resolve-loader.js @@ -0,0 +1,15 @@ +/** + * Node.js ESM loader hook for the JS → TS gradual migration. + * + * When a .js import specifier can't be found on disk, this loader tries the + * corresponding .ts file. This lets plain .js files import from already- + * migrated .ts modules without changing their import specifiers. + * + * Usage: node --import ./scripts/ts-resolve-loader.js ... + * (or via NODE_OPTIONS / vitest poolOptions.execArgv) + */ + +import { register } from 'node:module'; + +const hooksURL = new URL('./ts-resolve-hooks.js', import.meta.url); +register(hooksURL.href, { parentURL: import.meta.url }); diff --git a/src/db/repository/base.js b/src/db/repository/base.js deleted file mode 100644 index 0ab4deac..00000000 --- a/src/db/repository/base.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Abstract Repository base class. - * - * Defines the contract for all graph data access. Every method throws - * "not implemented" by default — concrete subclasses override what they support. - */ -export class Repository { - // ── Node lookups ──────────────────────────────────────────────────── - /** @param {number} id @returns {object|undefined} */ - findNodeById(_id) { - throw new Error('not implemented'); - } - - /** @param {string} file @returns {object[]} */ - findNodesByFile(_file) { - throw new Error('not implemented'); - } - - /** @param {string} fileLike @returns {object[]} */ - findFileNodes(_fileLike) { - throw new Error('not implemented'); - } - - /** @param {string} namePattern @param {object} [opts] @returns {object[]} */ - findNodesWithFanIn(_namePattern, _opts) { - throw new Error('not implemented'); - } - - /** @returns {number} */ - countNodes() { - throw new Error('not implemented'); - } - - /** @returns {number} */ - countEdges() { - throw new Error('not implemented'); - } - - /** @returns {number} */ - countFiles() { - throw new Error('not implemented'); - } - - /** @param {string} name @param {string} kind @param {string} file @param {number} line @returns {number|undefined} */ - getNodeId(_name, _kind, _file, _line) { - throw new Error('not implemented'); - } - - /** @param {string} name @param {string} file @param {number} line @returns {number|undefined} */ - getFunctionNodeId(_name, _file, _line) { - throw new Error('not implemented'); - } - - /** @param {string} file @returns {{ id: number, name: string, kind: string, line: number }[]} */ - bulkNodeIdsByFile(_file) { - throw new Error('not implemented'); - } - - /** @param {number} parentId @returns {object[]} */ - findNodeChildren(_parentId) { - throw new Error('not implemented'); - } - - /** @param {string} scopeName @param {object} [opts] @returns {object[]} */ - findNodesByScope(_scopeName, _opts) { - throw new Error('not implemented'); - } - - /** @param {string} qualifiedName @param {object} [opts] @returns {object[]} */ - findNodeByQualifiedName(_qualifiedName, _opts) { - throw new Error('not implemented'); - } - - /** @param {object} [opts] @returns {object[]} */ - listFunctionNodes(_opts) { - throw new Error('not implemented'); - } - - /** @param {object} [opts] @returns {IterableIterator} */ - iterateFunctionNodes(_opts) { - throw new Error('not implemented'); - } - - /** @param {object} [opts] @returns {object[]} */ - findNodesForTriage(_opts) { - throw new Error('not implemented'); - } - - // ── Edge queries ──────────────────────────────────────────────────── - /** @param {number} nodeId @returns {object[]} */ - findCallees(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object[]} */ - findCallers(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object[]} */ - findDistinctCallers(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object[]} */ - findAllOutgoingEdges(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object[]} */ - findAllIncomingEdges(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {string[]} */ - findCalleeNames(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {string[]} */ - findCallerNames(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */ - findImportTargets(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */ - findImportSources(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object[]} */ - findImportDependents(_nodeId) { - throw new Error('not implemented'); - } - - /** @param {string} file @returns {Set} */ - findCrossFileCallTargets(_file) { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @param {string} file @returns {number} */ - countCrossFileCallers(_nodeId, _file) { - throw new Error('not implemented'); - } - - /** @param {number} classNodeId @returns {Set} */ - getClassHierarchy(_classNodeId) { - throw new Error('not implemented'); - } - - /** @param {string} file @returns {{ caller_name: string, callee_name: string }[]} */ - findIntraFileCallEdges(_file) { - throw new Error('not implemented'); - } - - // ── Graph-read queries ────────────────────────────────────────────── - /** @returns {{ id: number, name: string, kind: string, file: string }[]} */ - getCallableNodes() { - throw new Error('not implemented'); - } - - /** @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */ - getCallEdges() { - throw new Error('not implemented'); - } - - /** @returns {{ id: number, name: string, file: string }[]} */ - getFileNodesAll() { - throw new Error('not implemented'); - } - - /** @returns {{ source_id: number, target_id: number }[]} */ - getImportEdges() { - throw new Error('not implemented'); - } - - // ── Optional table checks (default: false/undefined) ──────────────── - /** @returns {boolean} */ - hasCfgTables() { - throw new Error('not implemented'); - } - - /** @returns {boolean} */ - hasEmbeddings() { - throw new Error('not implemented'); - } - - /** @returns {boolean} */ - hasDataflowTable() { - throw new Error('not implemented'); - } - - /** @param {number} nodeId @returns {object|undefined} */ - getComplexityForNode(_nodeId) { - throw new Error('not implemented'); - } -} diff --git a/src/db/repository/base.ts b/src/db/repository/base.ts new file mode 100644 index 00000000..2a78902c --- /dev/null +++ b/src/db/repository/base.ts @@ -0,0 +1,190 @@ +import type { + AdjacentEdgeRow, + CallableNodeRow, + CallEdgeRow, + ChildNodeRow, + ComplexityMetrics, + FileNodeRow, + ImportEdgeRow, + ImportGraphEdgeRow, + IntraFileCallEdge, + ListFunctionOpts, + NodeIdRow, + NodeRow, + NodeRowWithFanIn, + QueryOpts, + RelatedNodeRow, + TriageQueryOpts, +} from '../../types.js'; + +/** + * Abstract Repository base class. + * + * Defines the contract for all graph data access. Every method throws + * "not implemented" by default — concrete subclasses override what they support. + */ +export class Repository { + // ── Node lookups ──────────────────────────────────────────────────── + findNodeById(_id: number): NodeRow | undefined { + throw new Error('not implemented'); + } + + findNodesByFile(_file: string): NodeRow[] { + throw new Error('not implemented'); + } + + findFileNodes(_fileLike: string): NodeRow[] { + throw new Error('not implemented'); + } + + findNodesWithFanIn(_namePattern: string, _opts?: QueryOpts): NodeRowWithFanIn[] { + throw new Error('not implemented'); + } + + countNodes(): number { + throw new Error('not implemented'); + } + + countEdges(): number { + throw new Error('not implemented'); + } + + countFiles(): number { + throw new Error('not implemented'); + } + + getNodeId(_name: string, _kind: string, _file: string, _line: number): number | undefined { + throw new Error('not implemented'); + } + + getFunctionNodeId(_name: string, _file: string, _line: number): number | undefined { + throw new Error('not implemented'); + } + + bulkNodeIdsByFile(_file: string): NodeIdRow[] { + throw new Error('not implemented'); + } + + findNodeChildren(_parentId: number): ChildNodeRow[] { + throw new Error('not implemented'); + } + + findNodesByScope(_scopeName: string, _opts?: QueryOpts): NodeRow[] { + throw new Error('not implemented'); + } + + findNodeByQualifiedName(_qualifiedName: string, _opts?: { file?: string }): NodeRow[] { + throw new Error('not implemented'); + } + + listFunctionNodes(_opts?: ListFunctionOpts): NodeRow[] { + throw new Error('not implemented'); + } + + iterateFunctionNodes(_opts?: ListFunctionOpts): IterableIterator { + throw new Error('not implemented'); + } + + findNodesForTriage(_opts?: TriageQueryOpts): NodeRow[] { + throw new Error('not implemented'); + } + + // ── Edge queries ──────────────────────────────────────────────────── + findCallees(_nodeId: number): RelatedNodeRow[] { + throw new Error('not implemented'); + } + + findCallers(_nodeId: number): RelatedNodeRow[] { + throw new Error('not implemented'); + } + + findDistinctCallers(_nodeId: number): RelatedNodeRow[] { + throw new Error('not implemented'); + } + + findAllOutgoingEdges(_nodeId: number): AdjacentEdgeRow[] { + throw new Error('not implemented'); + } + + findAllIncomingEdges(_nodeId: number): AdjacentEdgeRow[] { + throw new Error('not implemented'); + } + + findCalleeNames(_nodeId: number): string[] { + throw new Error('not implemented'); + } + + findCallerNames(_nodeId: number): string[] { + throw new Error('not implemented'); + } + + findImportTargets(_nodeId: number): ImportEdgeRow[] { + throw new Error('not implemented'); + } + + findImportSources(_nodeId: number): ImportEdgeRow[] { + throw new Error('not implemented'); + } + + findImportDependents(_nodeId: number): NodeRow[] { + throw new Error('not implemented'); + } + + findCrossFileCallTargets(_file: string): Set { + throw new Error('not implemented'); + } + + countCrossFileCallers(_nodeId: number, _file: string): number { + throw new Error('not implemented'); + } + + getClassHierarchy(_classNodeId: number): Set { + throw new Error('not implemented'); + } + + findImplementors(_nodeId: number): RelatedNodeRow[] { + throw new Error('not implemented'); + } + + findInterfaces(_nodeId: number): RelatedNodeRow[] { + throw new Error('not implemented'); + } + + findIntraFileCallEdges(_file: string): IntraFileCallEdge[] { + throw new Error('not implemented'); + } + + // ── Graph-read queries ────────────────────────────────────────────── + getCallableNodes(): CallableNodeRow[] { + throw new Error('not implemented'); + } + + getCallEdges(): CallEdgeRow[] { + throw new Error('not implemented'); + } + + getFileNodesAll(): FileNodeRow[] { + throw new Error('not implemented'); + } + + getImportEdges(): ImportGraphEdgeRow[] { + throw new Error('not implemented'); + } + + // ── Optional table checks (default: false/undefined) ──────────────── + hasCfgTables(): boolean { + throw new Error('not implemented'); + } + + hasEmbeddings(): boolean { + throw new Error('not implemented'); + } + + hasDataflowTable(): boolean { + throw new Error('not implemented'); + } + + getComplexityForNode(_nodeId: number): ComplexityMetrics | undefined { + throw new Error('not implemented'); + } +} diff --git a/src/db/repository/build-stmts.js b/src/db/repository/build-stmts.ts similarity index 72% rename from src/db/repository/build-stmts.js rename to src/db/repository/build-stmts.ts index 06b10772..fcad4a11 100644 --- a/src/db/repository/build-stmts.js +++ b/src/db/repository/build-stmts.ts @@ -1,13 +1,29 @@ +import type { BetterSqlite3Database, SqliteStatement } from '../../types.js'; + +interface PurgeStmts { + embeddings: SqliteStatement | null; + cfgEdges: SqliteStatement | null; + cfgBlocks: SqliteStatement | null; + dataflow: SqliteStatement | null; + complexity: SqliteStatement | null; + nodeMetrics: SqliteStatement | null; + astNodes: SqliteStatement | null; + edges: SqliteStatement; + nodes: SqliteStatement; + fileHashes: SqliteStatement | null; +} + +interface PurgeOpts { + purgeHashes?: boolean; +} + /** * Prepare all purge statements once, returning an object of runnable stmts. * Optional tables are wrapped in try/catch — if the table doesn't exist, * that slot is set to null. - * - * @param {object} db - Open read-write database handle - * @returns {object} prepared statements (some may be null) */ -function preparePurgeStmts(db) { - const tryPrepare = (sql) => { +function preparePurgeStmts(db: BetterSqlite3Database): PurgeStmts { + const tryPrepare = (sql: string): SqliteStatement | null => { try { return db.prepare(sql); } catch { @@ -47,25 +63,16 @@ function preparePurgeStmts(db) { /** * Cascade-delete all graph data for a single file across all tables. * Order: dependent tables first, then edges, then nodes, then hashes. - * - * @param {object} db - Open read-write database handle - * @param {string} file - Relative file path to purge - * @param {object} [opts] - * @param {boolean} [opts.purgeHashes=true] - Also delete file_hashes entry */ -export function purgeFileData(db, file, opts = {}) { +export function purgeFileData(db: BetterSqlite3Database, file: string, opts: PurgeOpts = {}): void { const stmts = preparePurgeStmts(db); runPurge(stmts, file, opts); } /** * Run purge using pre-prepared statements for a single file. - * @param {object} stmts - Prepared statements from preparePurgeStmts - * @param {string} file - Relative file path to purge - * @param {object} [opts] - * @param {boolean} [opts.purgeHashes=true] */ -function runPurge(stmts, file, opts = {}) { +function runPurge(stmts: PurgeStmts, file: string, opts: PurgeOpts = {}): void { const { purgeHashes = true } = opts; // Optional tables @@ -89,13 +96,12 @@ function runPurge(stmts, file, opts = {}) { /** * Purge all graph data for multiple files. * Prepares statements once and loops over files for efficiency. - * - * @param {object} db - Open read-write database handle - * @param {string[]} files - Relative file paths to purge - * @param {object} [opts] - * @param {boolean} [opts.purgeHashes=true] */ -export function purgeFilesData(db, files, opts = {}) { +export function purgeFilesData( + db: BetterSqlite3Database, + files: string[], + opts: PurgeOpts = {}, +): void { if (!files || files.length === 0) return; const stmts = preparePurgeStmts(db); for (const file of files) { diff --git a/src/db/repository/cached-stmt.js b/src/db/repository/cached-stmt.ts similarity index 55% rename from src/db/repository/cached-stmt.js rename to src/db/repository/cached-stmt.ts index 6a3ef8cb..2ceaa0da 100644 --- a/src/db/repository/cached-stmt.js +++ b/src/db/repository/cached-stmt.ts @@ -1,18 +1,19 @@ +import type { BetterSqlite3Database, SqliteStatement, StmtCache } from '../../types.js'; + /** * Resolve a cached prepared statement, compiling on first use per db. * Each `cache` WeakMap must always be called with the same `sql` — * the sql argument is only used on the first compile; subsequent calls * return the cached statement regardless of the sql passed. - * - * @param {WeakMap} cache - WeakMap keyed by db instance - * @param {object} db - better-sqlite3 database instance - * @param {string} sql - SQL to compile on first use - * @returns {object} prepared statement */ -export function cachedStmt(cache, db, sql) { +export function cachedStmt( + cache: StmtCache, + db: BetterSqlite3Database, + sql: string, +): SqliteStatement { let stmt = cache.get(db); if (!stmt) { - stmt = db.prepare(sql); + stmt = db.prepare(sql); cache.set(db, stmt); } return stmt; diff --git a/src/db/repository/cfg.js b/src/db/repository/cfg.ts similarity index 59% rename from src/db/repository/cfg.js rename to src/db/repository/cfg.ts index 42fe4b7a..5572d2ec 100644 --- a/src/db/repository/cfg.js +++ b/src/db/repository/cfg.ts @@ -1,17 +1,34 @@ +import type { BetterSqlite3Database, StmtCache } from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Statement caches (one prepared statement per db instance) ──────────── -const _getCfgBlocksStmt = new WeakMap(); -const _getCfgEdgesStmt = new WeakMap(); -const _deleteCfgEdgesStmt = new WeakMap(); -const _deleteCfgBlocksStmt = new WeakMap(); + +interface CfgBlockRow { + id: number; + block_index: number; + block_type: string; + start_line: number; + end_line: number; + label: string | null; +} + +interface CfgEdgeRow { + kind: string; + source_index: number; + source_type: string; + target_index: number; + target_type: string; +} + +const _getCfgBlocksStmt: StmtCache = new WeakMap(); +const _getCfgEdgesStmt: StmtCache = new WeakMap(); +const _deleteCfgEdgesStmt: StmtCache = new WeakMap(); +const _deleteCfgBlocksStmt: StmtCache = new WeakMap(); /** * Check whether CFG tables exist. - * @param {object} db - * @returns {boolean} */ -export function hasCfgTables(db) { +export function hasCfgTables(db: BetterSqlite3Database): boolean { try { db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get(); return true; @@ -22,11 +39,8 @@ export function hasCfgTables(db) { /** * Get CFG blocks for a function node. - * @param {object} db - * @param {number} functionNodeId - * @returns {object[]} */ -export function getCfgBlocks(db, functionNodeId) { +export function getCfgBlocks(db: BetterSqlite3Database, functionNodeId: number): CfgBlockRow[] { return cachedStmt( _getCfgBlocksStmt, db, @@ -38,11 +52,8 @@ export function getCfgBlocks(db, functionNodeId) { /** * Get CFG edges for a function node (with block info). - * @param {object} db - * @param {number} functionNodeId - * @returns {object[]} */ -export function getCfgEdges(db, functionNodeId) { +export function getCfgEdges(db: BetterSqlite3Database, functionNodeId: number): CfgEdgeRow[] { return cachedStmt( _getCfgEdgesStmt, db, @@ -59,10 +70,8 @@ export function getCfgEdges(db, functionNodeId) { /** * Delete all CFG data for a function node. - * @param {object} db - * @param {number} functionNodeId */ -export function deleteCfgForNode(db, functionNodeId) { +export function deleteCfgForNode(db: BetterSqlite3Database, functionNodeId: number): void { cachedStmt(_deleteCfgEdgesStmt, db, 'DELETE FROM cfg_edges WHERE function_node_id = ?').run( functionNodeId, ); diff --git a/src/db/repository/cochange.js b/src/db/repository/cochange.ts similarity index 60% rename from src/db/repository/cochange.js rename to src/db/repository/cochange.ts index c5a51eee..449c78e5 100644 --- a/src/db/repository/cochange.js +++ b/src/db/repository/cochange.ts @@ -1,16 +1,15 @@ +import type { BetterSqlite3Database, StmtCache } from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Statement caches (one prepared statement per db instance) ──────────── -const _hasCoChangesStmt = new WeakMap(); -const _getCoChangeMetaStmt = new WeakMap(); -const _upsertCoChangeMetaStmt = new WeakMap(); +const _hasCoChangesStmt: StmtCache<{ 1: number }> = new WeakMap(); +const _getCoChangeMetaStmt: StmtCache<{ key: string; value: string }> = new WeakMap(); +const _upsertCoChangeMetaStmt: StmtCache = new WeakMap(); /** * Check whether the co_changes table has data. - * @param {object} db - * @returns {boolean} */ -export function hasCoChanges(db) { +export function hasCoChanges(db: BetterSqlite3Database): boolean { try { return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get(); } catch { @@ -20,11 +19,9 @@ export function hasCoChanges(db) { /** * Get all co-change metadata as a key-value map. - * @param {object} db - * @returns {Record} */ -export function getCoChangeMeta(db) { - const meta = {}; +export function getCoChangeMeta(db: BetterSqlite3Database): Record { + const meta: Record = {}; try { for (const row of cachedStmt( _getCoChangeMetaStmt, @@ -41,11 +38,8 @@ export function getCoChangeMeta(db) { /** * Upsert a co-change metadata key-value pair. - * @param {object} db - * @param {string} key - * @param {string} value */ -export function upsertCoChangeMeta(db, key, value) { +export function upsertCoChangeMeta(db: BetterSqlite3Database, key: string, value: string): void { cachedStmt( _upsertCoChangeMetaStmt, db, diff --git a/src/db/repository/complexity.js b/src/db/repository/complexity.ts similarity index 62% rename from src/db/repository/complexity.js rename to src/db/repository/complexity.ts index f65808bb..9d2b04df 100644 --- a/src/db/repository/complexity.js +++ b/src/db/repository/complexity.ts @@ -1,16 +1,17 @@ +import type { BetterSqlite3Database, ComplexityMetrics, StmtCache } from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Statement caches (one prepared statement per db instance) ──────────── -const _getComplexityForNodeStmt = new WeakMap(); +const _getComplexityForNodeStmt: StmtCache = new WeakMap(); /** * Get complexity metrics for a node. * Used by contextData and explainFunctionImpl in queries.js. - * @param {object} db - * @param {number} nodeId - * @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined} */ -export function getComplexityForNode(db, nodeId) { +export function getComplexityForNode( + db: BetterSqlite3Database, + nodeId: number, +): ComplexityMetrics | undefined { return cachedStmt( _getComplexityForNodeStmt, db, diff --git a/src/db/repository/dataflow.js b/src/db/repository/dataflow.js deleted file mode 100644 index 4cf8eb16..00000000 --- a/src/db/repository/dataflow.js +++ /dev/null @@ -1,17 +0,0 @@ -import { cachedStmt } from './cached-stmt.js'; - -// ─── Statement caches (one prepared statement per db instance) ──────────── -const _hasDataflowTableStmt = new WeakMap(); - -/** - * Check whether the dataflow table exists and has data. - * @param {object} db - * @returns {boolean} - */ -export function hasDataflowTable(db) { - try { - return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0; - } catch { - return false; - } -} diff --git a/src/db/repository/dataflow.ts b/src/db/repository/dataflow.ts new file mode 100644 index 00000000..58e00e59 --- /dev/null +++ b/src/db/repository/dataflow.ts @@ -0,0 +1,19 @@ +import type { BetterSqlite3Database, StmtCache } from '../../types.js'; +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _hasDataflowTableStmt: StmtCache<{ c: number }> = new WeakMap(); + +/** + * Check whether the dataflow table exists and has data. + */ +export function hasDataflowTable(db: BetterSqlite3Database): boolean { + try { + return ( + (cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get()?.c ?? 0) > + 0 + ); + } catch { + return false; + } +} diff --git a/src/db/repository/edges.js b/src/db/repository/edges.ts similarity index 61% rename from src/db/repository/edges.js rename to src/db/repository/edges.ts index 81902d43..5ddf1fe3 100644 --- a/src/db/repository/edges.js +++ b/src/db/repository/edges.ts @@ -1,33 +1,38 @@ +import type { + AdjacentEdgeRow, + BetterSqlite3Database, + ImportEdgeRow, + IntraFileCallEdge, + NodeRow, + RelatedNodeRow, + StmtCache, +} from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Prepared-statement caches (one per db instance) ──────────────────── -const _findCalleesStmt = new WeakMap(); -const _findCallersStmt = new WeakMap(); -const _findDistinctCallersStmt = new WeakMap(); -const _findAllOutgoingStmt = new WeakMap(); -const _findAllIncomingStmt = new WeakMap(); -const _findCalleeNamesStmt = new WeakMap(); -const _findCallerNamesStmt = new WeakMap(); -const _findImportTargetsStmt = new WeakMap(); -const _findImportSourcesStmt = new WeakMap(); -const _findImportDependentsStmt = new WeakMap(); -const _findCrossFileCallTargetsStmt = new WeakMap(); -const _countCrossFileCallersStmt = new WeakMap(); -const _getClassAncestorsStmt = new WeakMap(); -const _findIntraFileCallEdgesStmt = new WeakMap(); -const _findImplementorsStmt = new WeakMap(); -const _findInterfacesStmt = new WeakMap(); +const _findCalleesStmt: StmtCache = new WeakMap(); +const _findCallersStmt: StmtCache = new WeakMap(); +const _findDistinctCallersStmt: StmtCache = new WeakMap(); +const _findAllOutgoingStmt: StmtCache = new WeakMap(); +const _findAllIncomingStmt: StmtCache = new WeakMap(); +const _findCalleeNamesStmt: StmtCache<{ name: string }> = new WeakMap(); +const _findCallerNamesStmt: StmtCache<{ name: string }> = new WeakMap(); +const _findImportTargetsStmt: StmtCache = new WeakMap(); +const _findImportSourcesStmt: StmtCache = new WeakMap(); +const _findImportDependentsStmt: StmtCache = new WeakMap(); +const _findCrossFileCallTargetsStmt: StmtCache<{ target_id: number }> = new WeakMap(); +const _countCrossFileCallersStmt: StmtCache<{ cnt: number }> = new WeakMap(); +const _getClassAncestorsStmt: StmtCache<{ id: number; name: string }> = new WeakMap(); +const _findIntraFileCallEdgesStmt: StmtCache = new WeakMap(); +const _findImplementorsStmt: StmtCache = new WeakMap(); +const _findInterfacesStmt: StmtCache = new WeakMap(); // ─── Call-edge queries ────────────────────────────────────────────────── /** * Find all callees of a node (outgoing 'calls' edges). - * Returns full node info including end_line for source display. - * @param {object} db - * @param {number} nodeId - * @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]} */ -export function findCallees(db, nodeId) { +export function findCallees(db: BetterSqlite3Database, nodeId: number): RelatedNodeRow[] { return cachedStmt( _findCalleesStmt, db, @@ -39,11 +44,8 @@ export function findCallees(db, nodeId) { /** * Find all callers of a node (incoming 'calls' edges). - * @param {object} db - * @param {number} nodeId - * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ -export function findCallers(db, nodeId) { +export function findCallers(db: BetterSqlite3Database, nodeId: number): RelatedNodeRow[] { return cachedStmt( _findCallersStmt, db, @@ -55,11 +57,8 @@ export function findCallers(db, nodeId) { /** * Find distinct callers of a node (for impact analysis BFS). - * @param {object} db - * @param {number} nodeId - * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ -export function findDistinctCallers(db, nodeId) { +export function findDistinctCallers(db: BetterSqlite3Database, nodeId: number): RelatedNodeRow[] { return cachedStmt( _findDistinctCallersStmt, db, @@ -73,11 +72,8 @@ export function findDistinctCallers(db, nodeId) { /** * Find all outgoing edges with edge kind (for queryNameData). - * @param {object} db - * @param {number} nodeId - * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]} */ -export function findAllOutgoingEdges(db, nodeId) { +export function findAllOutgoingEdges(db: BetterSqlite3Database, nodeId: number): AdjacentEdgeRow[] { return cachedStmt( _findAllOutgoingStmt, db, @@ -89,11 +85,8 @@ export function findAllOutgoingEdges(db, nodeId) { /** * Find all incoming edges with edge kind (for queryNameData). - * @param {object} db - * @param {number} nodeId - * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]} */ -export function findAllIncomingEdges(db, nodeId) { +export function findAllIncomingEdges(db: BetterSqlite3Database, nodeId: number): AdjacentEdgeRow[] { return cachedStmt( _findAllIncomingStmt, db, @@ -107,11 +100,8 @@ export function findAllIncomingEdges(db, nodeId) { /** * Get distinct callee names for a node, sorted alphabetically. - * @param {object} db - * @param {number} nodeId - * @returns {string[]} */ -export function findCalleeNames(db, nodeId) { +export function findCalleeNames(db: BetterSqlite3Database, nodeId: number): string[] { return cachedStmt( _findCalleeNamesStmt, db, @@ -126,11 +116,8 @@ export function findCalleeNames(db, nodeId) { /** * Get distinct caller names for a node, sorted alphabetically. - * @param {object} db - * @param {number} nodeId - * @returns {string[]} */ -export function findCallerNames(db, nodeId) { +export function findCallerNames(db: BetterSqlite3Database, nodeId: number): string[] { return cachedStmt( _findCallerNamesStmt, db, @@ -147,11 +134,8 @@ export function findCallerNames(db, nodeId) { /** * Find outgoing import edges (files this node imports). - * @param {object} db - * @param {number} nodeId - * @returns {{ file: string, edge_kind: string }[]} */ -export function findImportTargets(db, nodeId) { +export function findImportTargets(db: BetterSqlite3Database, nodeId: number): ImportEdgeRow[] { return cachedStmt( _findImportTargetsStmt, db, @@ -163,11 +147,8 @@ export function findImportTargets(db, nodeId) { /** * Find incoming import edges (files that import this node). - * @param {object} db - * @param {number} nodeId - * @returns {{ file: string, edge_kind: string }[]} */ -export function findImportSources(db, nodeId) { +export function findImportSources(db: BetterSqlite3Database, nodeId: number): ImportEdgeRow[] { return cachedStmt( _findImportSourcesStmt, db, @@ -179,12 +160,8 @@ export function findImportSources(db, nodeId) { /** * Find nodes that import a given node (BFS-ready, returns full node info). - * Used by impactAnalysisData for transitive import traversal. - * @param {object} db - * @param {number} nodeId - * @returns {object[]} */ -export function findImportDependents(db, nodeId) { +export function findImportDependents(db: BetterSqlite3Database, nodeId: number): NodeRow[] { return cachedStmt( _findImportDependentsStmt, db, @@ -197,12 +174,8 @@ export function findImportDependents(db, nodeId) { /** * Get IDs of symbols in a file that are called from other files. - * Used for "exported" detection in explain/where/exports. - * @param {object} db - * @param {string} file - * @returns {Set} */ -export function findCrossFileCallTargets(db, file) { +export function findCrossFileCallTargets(db: BetterSqlite3Database, file: string): Set { return new Set( cachedStmt( _findCrossFileCallTargetsStmt, @@ -219,29 +192,27 @@ export function findCrossFileCallTargets(db, file) { /** * Count callers that are in a different file than the target node. - * Used by whereSymbolImpl to determine if a symbol is exported. - * @param {object} db - * @param {number} nodeId - * @param {string} file - The target node's file - * @returns {number} */ -export function countCrossFileCallers(db, nodeId, file) { - return cachedStmt( - _countCrossFileCallersStmt, - db, - `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id +export function countCrossFileCallers( + db: BetterSqlite3Database, + nodeId: number, + file: string, +): number { + return ( + cachedStmt( + _countCrossFileCallersStmt, + db, + `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`, - ).get(nodeId, file).cnt; + ).get(nodeId, file)?.cnt ?? 0 + ); } /** * Get all ancestor class IDs via extends edges (BFS). - * @param {object} db - * @param {number} classNodeId - * @returns {Set} */ -export function getClassHierarchy(db, classNodeId) { - const ancestors = new Set(); +export function getClassHierarchy(db: BetterSqlite3Database, classNodeId: number): Set { + const ancestors = new Set(); const queue = [classNodeId]; const stmt = cachedStmt( _getClassAncestorsStmt, @@ -250,7 +221,7 @@ export function getClassHierarchy(db, classNodeId) { WHERE e.source_id = ? AND e.kind = 'extends'`, ); while (queue.length > 0) { - const current = queue.shift(); + const current = queue.shift() as number; const parents = stmt.all(current); for (const p of parents) { if (!ancestors.has(p.id)) { @@ -266,12 +237,8 @@ export function getClassHierarchy(db, classNodeId) { /** * Find all concrete types that implement a given interface/trait node. - * Follows incoming 'implements' edges (source = implementor, target = interface). - * @param {object} db - * @param {number} nodeId - The interface/trait node ID - * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ -export function findImplementors(db, nodeId) { +export function findImplementors(db: BetterSqlite3Database, nodeId: number): RelatedNodeRow[] { return cachedStmt( _findImplementorsStmt, db, @@ -283,12 +250,8 @@ export function findImplementors(db, nodeId) { /** * Find all interfaces/traits that a given class/struct implements. - * Follows outgoing 'implements' edges (source = class, target = interface). - * @param {object} db - * @param {number} nodeId - The class/struct node ID - * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ -export function findInterfaces(db, nodeId) { +export function findInterfaces(db: BetterSqlite3Database, nodeId: number): RelatedNodeRow[] { return cachedStmt( _findInterfacesStmt, db, @@ -300,12 +263,11 @@ export function findInterfaces(db, nodeId) { /** * Find intra-file call edges (caller → callee within the same file). - * Used by explainFileImpl for data flow visualization. - * @param {object} db - * @param {string} file - * @returns {{ caller_name: string, callee_name: string }[]} */ -export function findIntraFileCallEdges(db, file) { +export function findIntraFileCallEdges( + db: BetterSqlite3Database, + file: string, +): IntraFileCallEdge[] { return cachedStmt( _findIntraFileCallEdgesStmt, db, diff --git a/src/db/repository/embeddings.js b/src/db/repository/embeddings.ts similarity index 52% rename from src/db/repository/embeddings.js rename to src/db/repository/embeddings.ts index 9a5af373..4d891632 100644 --- a/src/db/repository/embeddings.js +++ b/src/db/repository/embeddings.ts @@ -1,16 +1,15 @@ +import type { BetterSqlite3Database, StmtCache } from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Statement caches (one prepared statement per db instance) ──────────── -const _hasEmbeddingsStmt = new WeakMap(); -const _getEmbeddingCountStmt = new WeakMap(); -const _getEmbeddingMetaStmt = new WeakMap(); +const _hasEmbeddingsStmt: StmtCache<{ 1: number }> = new WeakMap(); +const _getEmbeddingCountStmt: StmtCache<{ c: number }> = new WeakMap(); +const _getEmbeddingMetaStmt: StmtCache<{ value: string }> = new WeakMap(); /** * Check whether the embeddings table has data. - * @param {object} db - * @returns {boolean} */ -export function hasEmbeddings(db) { +export function hasEmbeddings(db: BetterSqlite3Database): boolean { try { return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get(); } catch { @@ -20,12 +19,12 @@ export function hasEmbeddings(db) { /** * Get the count of embeddings. - * @param {object} db - * @returns {number} */ -export function getEmbeddingCount(db) { +export function getEmbeddingCount(db: BetterSqlite3Database): number { try { - return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c; + return ( + cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get()?.c ?? 0 + ); } catch { return 0; } @@ -33,11 +32,8 @@ export function getEmbeddingCount(db) { /** * Get a single embedding metadata value by key. - * @param {object} db - * @param {string} key - * @returns {string|undefined} */ -export function getEmbeddingMeta(db, key) { +export function getEmbeddingMeta(db: BetterSqlite3Database, key: string): string | undefined { try { const row = cachedStmt( _getEmbeddingMetaStmt, diff --git a/src/db/repository/graph-read.js b/src/db/repository/graph-read.ts similarity index 54% rename from src/db/repository/graph-read.js rename to src/db/repository/graph-read.ts index 5538a9d4..5dc0e918 100644 --- a/src/db/repository/graph-read.js +++ b/src/db/repository/graph-read.ts @@ -1,20 +1,26 @@ import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js'; +import type { + BetterSqlite3Database, + CallableNodeRow, + CallEdgeRow, + FileNodeRow, + ImportGraphEdgeRow, + StmtCache, +} from '../../types.js'; import { cachedStmt } from './cached-stmt.js'; // ─── Statement caches (one prepared statement per db instance) ──────────── -const _getCallableNodesStmt = new WeakMap(); -const _getCallEdgesStmt = new WeakMap(); -const _getFileNodesAllStmt = new WeakMap(); -const _getImportEdgesStmt = new WeakMap(); +const _getCallableNodesStmt: StmtCache = new WeakMap(); +const _getCallEdgesStmt: StmtCache = new WeakMap(); +const _getFileNodesAllStmt: StmtCache = new WeakMap(); +const _getImportEdgesStmt: StmtCache = new WeakMap(); -const CALLABLE_KINDS_SQL = CORE_SYMBOL_KINDS.map((k) => `'${k}'`).join(','); +const CALLABLE_KINDS_SQL = CORE_SYMBOL_KINDS.map((k: string) => `'${k}'`).join(','); /** * Get callable nodes (all core symbol kinds) for graph construction. - * @param {object} db - * @returns {{ id: number, name: string, kind: string, file: string }[]} */ -export function getCallableNodes(db) { +export function getCallableNodes(db: BetterSqlite3Database): CallableNodeRow[] { return cachedStmt( _getCallableNodesStmt, db, @@ -24,10 +30,8 @@ export function getCallableNodes(db) { /** * Get all 'calls' edges. - * @param {object} db - * @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */ -export function getCallEdges(db) { +export function getCallEdges(db: BetterSqlite3Database): CallEdgeRow[] { return cachedStmt( _getCallEdgesStmt, db, @@ -37,10 +41,8 @@ export function getCallEdges(db) { /** * Get all file-kind nodes. - * @param {object} db - * @returns {{ id: number, name: string, file: string }[]} */ -export function getFileNodesAll(db) { +export function getFileNodesAll(db: BetterSqlite3Database): FileNodeRow[] { return cachedStmt( _getFileNodesAllStmt, db, @@ -50,10 +52,8 @@ export function getFileNodesAll(db) { /** * Get all import edges. - * @param {object} db - * @returns {{ source_id: number, target_id: number }[]} */ -export function getImportEdges(db) { +export function getImportEdges(db: BetterSqlite3Database): ImportGraphEdgeRow[] { return cachedStmt( _getImportEdgesStmt, db, diff --git a/src/db/repository/in-memory-repository.js b/src/db/repository/in-memory-repository.ts similarity index 72% rename from src/db/repository/in-memory-repository.js rename to src/db/repository/in-memory-repository.ts index 5f2779d1..f2667e35 100644 --- a/src/db/repository/in-memory-repository.js +++ b/src/db/repository/in-memory-repository.ts @@ -5,22 +5,42 @@ import { EVERY_SYMBOL_KIND, VALID_ROLES, } from '../../shared/kinds.js'; +import type { + AdjacentEdgeRow, + AnyEdgeKind, + AnyNodeKind, + CallableNodeRow, + CallEdgeRow, + ChildNodeRow, + ComplexityMetrics, + EdgeRow, + FileNodeRow, + ImportEdgeRow, + ImportGraphEdgeRow, + IntraFileCallEdge, + ListFunctionOpts, + NodeIdRow, + NodeRow, + NodeRowWithFanIn, + QueryOpts, + RelatedNodeRow, + Role, + TriageQueryOpts, +} from '../../types.js'; import { escapeLike, normalizeFileFilter } from '../query-builder.js'; import { Repository } from './base.js'; /** * Convert a SQL LIKE pattern to a RegExp (case-insensitive). * Supports `%` (any chars) and `_` (single char). - * @param {string} pattern - * @returns {RegExp} */ -function likeToRegex(pattern) { +function likeToRegex(pattern: string): RegExp { let regex = ''; for (let i = 0; i < pattern.length; i++) { - const ch = pattern[i]; + const ch = pattern[i]!; if (ch === '\\' && i + 1 < pattern.length) { // Escaped literal - regex += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + regex += pattern[++i]?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } else if (ch === '%') { regex += '.*'; } else if (ch === '_') { @@ -36,11 +56,13 @@ function likeToRegex(pattern) { * Build a filter predicate for file matching. * Accepts string, string[], or falsy. Returns null when no filtering needed. */ -function buildFileFilterFn(file) { +function buildFileFilterFn( + file: string | string[] | undefined, +): ((filePath: string) => boolean) | null { const files = normalizeFileFilter(file); if (files.length === 0) return null; - const regexes = files.map((f) => likeToRegex(`%${escapeLike(f)}%`)); - return (filePath) => regexes.some((re) => re.test(filePath)); + const regexes = files.map((f: string) => likeToRegex(`%${escapeLike(f)}%`)); + return (filePath: string) => regexes.some((re: RegExp) => re.test(filePath)); } /** @@ -48,9 +70,9 @@ function buildFileFilterFn(file) { * No SQLite dependency — suitable for fast unit tests. */ export class InMemoryRepository extends Repository { - #nodes = new Map(); // id → node object - #edges = new Map(); // id → edge object - #complexity = new Map(); // node_id → complexity metrics + #nodes = new Map(); + #edges = new Map(); + #complexity = new Map(); #nextNodeId = 1; #nextEdgeId = 1; @@ -58,10 +80,20 @@ export class InMemoryRepository extends Repository { /** * Add a node. Returns the auto-assigned id. - * @param {object} attrs - { name, kind, file, line, end_line?, parent_id?, exported?, qualified_name?, scope?, visibility?, role? } - * @returns {number} */ - addNode(attrs) { + addNode(attrs: { + name: string; + kind: AnyNodeKind; + file: string; + line: number; + end_line?: number; + parent_id?: number; + exported?: 0 | 1; + qualified_name?: string; + scope?: string; + visibility?: 'public' | 'private' | 'protected'; + role?: Role; + }): number { const id = this.#nextNodeId++; this.#nodes.set(id, { id, @@ -82,16 +114,20 @@ export class InMemoryRepository extends Repository { /** * Add an edge. Returns the auto-assigned id. - * @param {object} attrs - { source_id, target_id, kind, confidence?, dynamic? } - * @returns {number} */ - addEdge(attrs) { + addEdge(attrs: { + source_id: number; + target_id: number; + kind: AnyEdgeKind; + confidence?: number; + dynamic?: 0 | 1; + }): number { const id = this.#nextEdgeId++; this.#edges.set(id, { id, source_id: attrs.source_id, target_id: attrs.target_id, - kind: attrs.kind, + kind: attrs.kind as EdgeRow['kind'], confidence: attrs.confidence ?? null, dynamic: attrs.dynamic ?? 0, }); @@ -100,10 +136,17 @@ export class InMemoryRepository extends Repository { /** * Add complexity metrics for a node. - * @param {number} nodeId - * @param {object} metrics - { cognitive, cyclomatic, max_nesting, maintainability_index?, halstead_volume? } */ - addComplexity(nodeId, metrics) { + addComplexity( + nodeId: number, + metrics: { + cognitive: number; + cyclomatic: number; + max_nesting: number; + maintainability_index?: number; + halstead_volume?: number; + }, + ): void { this.#complexity.set(nodeId, { cognitive: metrics.cognitive ?? 0, cyclomatic: metrics.cyclomatic ?? 0, @@ -115,27 +158,29 @@ export class InMemoryRepository extends Repository { // ── Node lookups ────────────────────────────────────────────────── - findNodeById(id) { + findNodeById(id: number): NodeRow | undefined { return this.#nodes.get(id) ?? undefined; } - findNodesByFile(file) { + findNodesByFile(file: string): NodeRow[] { return [...this.#nodes.values()] .filter((n) => n.file === file && n.kind !== 'file') .sort((a, b) => a.line - b.line); } - findFileNodes(fileLike) { + findFileNodes(fileLike: string): NodeRow[] { const re = likeToRegex(fileLike); return [...this.#nodes.values()].filter((n) => n.kind === 'file' && re.test(n.file)); } - findNodesWithFanIn(namePattern, opts = {}) { + findNodesWithFanIn(namePattern: string, opts: QueryOpts = {}): NodeRowWithFanIn[] { const re = likeToRegex(namePattern); let nodes = [...this.#nodes.values()].filter((n) => re.test(n.name)); if (opts.kinds) { - nodes = nodes.filter((n) => opts.kinds.includes(n.kind)); + nodes = nodes.filter((n) => + opts.kinds?.includes(n.kind as typeof opts.kinds extends (infer U)[] ? U : never), + ); } { const fileFn = buildFileFilterFn(opts.file); @@ -147,23 +192,23 @@ export class InMemoryRepository extends Repository { return nodes.map((n) => ({ ...n, fan_in: fanInMap.get(n.id) ?? 0 })); } - countNodes() { + countNodes(): number { return this.#nodes.size; } - countEdges() { + countEdges(): number { return this.#edges.size; } - countFiles() { - const files = new Set(); + countFiles(): number { + const files = new Set(); for (const n of this.#nodes.values()) { files.add(n.file); } return files.size; } - getNodeId(name, kind, file, line) { + getNodeId(name: string, kind: string, file: string, line: number): number | undefined { for (const n of this.#nodes.values()) { if (n.name === name && n.kind === kind && n.file === file && n.line === line) { return n.id; @@ -172,7 +217,7 @@ export class InMemoryRepository extends Repository { return undefined; } - getFunctionNodeId(name, file, line) { + getFunctionNodeId(name: string, file: string, line: number): number | undefined { for (const n of this.#nodes.values()) { if ( n.name === name && @@ -186,19 +231,19 @@ export class InMemoryRepository extends Repository { return undefined; } - bulkNodeIdsByFile(file) { + bulkNodeIdsByFile(file: string): NodeIdRow[] { return [...this.#nodes.values()] .filter((n) => n.file === file) .map((n) => ({ id: n.id, name: n.name, kind: n.kind, line: n.line })); } - findNodeChildren(parentId) { + findNodeChildren(parentId: number): ChildNodeRow[] { return [...this.#nodes.values()] .filter((n) => n.parent_id === parentId) .sort((a, b) => a.line - b.line) .map((n) => ({ name: n.name, - kind: n.kind, + kind: n.kind as ChildNodeRow['kind'], line: n.line, end_line: n.end_line, qualified_name: n.qualified_name, @@ -207,7 +252,7 @@ export class InMemoryRepository extends Repository { })); } - findNodesByScope(scopeName, opts = {}) { + findNodesByScope(scopeName: string, opts: QueryOpts = {}): NodeRow[] { let nodes = [...this.#nodes.values()].filter((n) => n.scope === scopeName); if (opts.kind) { @@ -221,7 +266,7 @@ export class InMemoryRepository extends Repository { return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line); } - findNodeByQualifiedName(qualifiedName, opts = {}) { + findNodeByQualifiedName(qualifiedName: string, opts: { file?: string } = {}): NodeRow[] { let nodes = [...this.#nodes.values()].filter((n) => n.qualified_name === qualifiedName); { @@ -232,15 +277,15 @@ export class InMemoryRepository extends Repository { return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line); } - listFunctionNodes(opts = {}) { + listFunctionNodes(opts: ListFunctionOpts = {}): NodeRow[] { return [...this.#iterateFunctionNodesImpl(opts)]; } - *iterateFunctionNodes(opts = {}) { + *iterateFunctionNodes(opts: ListFunctionOpts = {}): IterableIterator { yield* this.#iterateFunctionNodesImpl(opts); } - findNodesForTriage(opts = {}) { + findNodesForTriage(opts: TriageQueryOpts = {}): NodeRow[] { if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) { throw new ConfigError( `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`, @@ -282,28 +327,22 @@ export class InMemoryRepository extends Repository { .map((n) => { const cx = this.#complexity.get(n.id); return { - id: n.id, - name: n.name, - kind: n.kind, - file: n.file, - line: n.line, - end_line: n.end_line, - role: n.role, + ...n, fan_in: fanInMap.get(n.id) ?? 0, cognitive: cx?.cognitive ?? 0, mi: cx?.maintainability_index ?? 0, cyclomatic: cx?.cyclomatic ?? 0, max_nesting: cx?.max_nesting ?? 0, churn: 0, // no co-change data in-memory - }; + } as NodeRow; }); } // ── Edge queries ────────────────────────────────────────────────── - findCallees(nodeId) { - const seen = new Set(); - const results = []; + findCallees(nodeId: number): RelatedNodeRow[] { + const seen = new Set(); + const results: RelatedNodeRow[] = []; for (const e of this.#edges.values()) { if (e.source_id === nodeId && e.kind === 'calls' && !seen.has(e.target_id)) { seen.add(e.target_id); @@ -322,8 +361,8 @@ export class InMemoryRepository extends Repository { return results; } - findCallers(nodeId) { - const results = []; + findCallers(nodeId: number): RelatedNodeRow[] { + const results: RelatedNodeRow[] = []; for (const e of this.#edges.values()) { if (e.target_id === nodeId && e.kind === 'calls') { const n = this.#nodes.get(e.source_id); @@ -333,9 +372,9 @@ export class InMemoryRepository extends Repository { return results; } - findDistinctCallers(nodeId) { - const seen = new Set(); - const results = []; + findDistinctCallers(nodeId: number): RelatedNodeRow[] { + const seen = new Set(); + const results: RelatedNodeRow[] = []; for (const e of this.#edges.values()) { if (e.target_id === nodeId && e.kind === 'calls' && !seen.has(e.source_id)) { seen.add(e.source_id); @@ -346,8 +385,8 @@ export class InMemoryRepository extends Repository { return results; } - findAllOutgoingEdges(nodeId) { - const results = []; + findAllOutgoingEdges(nodeId: number): AdjacentEdgeRow[] { + const results: AdjacentEdgeRow[] = []; for (const e of this.#edges.values()) { if (e.source_id === nodeId) { const n = this.#nodes.get(e.target_id); @@ -364,8 +403,8 @@ export class InMemoryRepository extends Repository { return results; } - findAllIncomingEdges(nodeId) { - const results = []; + findAllIncomingEdges(nodeId: number): AdjacentEdgeRow[] { + const results: AdjacentEdgeRow[] = []; for (const e of this.#edges.values()) { if (e.target_id === nodeId) { const n = this.#nodes.get(e.source_id); @@ -382,8 +421,8 @@ export class InMemoryRepository extends Repository { return results; } - findCalleeNames(nodeId) { - const names = new Set(); + findCalleeNames(nodeId: number): string[] { + const names = new Set(); for (const e of this.#edges.values()) { if (e.source_id === nodeId && e.kind === 'calls') { const n = this.#nodes.get(e.target_id); @@ -393,8 +432,8 @@ export class InMemoryRepository extends Repository { return [...names].sort(); } - findCallerNames(nodeId) { - const names = new Set(); + findCallerNames(nodeId: number): string[] { + const names = new Set(); for (const e of this.#edges.values()) { if (e.target_id === nodeId && e.kind === 'calls') { const n = this.#nodes.get(e.source_id); @@ -404,8 +443,8 @@ export class InMemoryRepository extends Repository { return [...names].sort(); } - findImportTargets(nodeId) { - const results = []; + findImportTargets(nodeId: number): ImportEdgeRow[] { + const results: ImportEdgeRow[] = []; for (const e of this.#edges.values()) { if (e.source_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) { const n = this.#nodes.get(e.target_id); @@ -415,8 +454,8 @@ export class InMemoryRepository extends Repository { return results; } - findImportSources(nodeId) { - const results = []; + findImportSources(nodeId: number): ImportEdgeRow[] { + const results: ImportEdgeRow[] = []; for (const e of this.#edges.values()) { if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) { const n = this.#nodes.get(e.source_id); @@ -426,8 +465,8 @@ export class InMemoryRepository extends Repository { return results; } - findImportDependents(nodeId) { - const results = []; + findImportDependents(nodeId: number): NodeRow[] { + const results: NodeRow[] = []; for (const e of this.#edges.values()) { if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) { const n = this.#nodes.get(e.source_id); @@ -437,8 +476,8 @@ export class InMemoryRepository extends Repository { return results; } - findCrossFileCallTargets(file) { - const targets = new Set(); + findCrossFileCallTargets(file: string): Set { + const targets = new Set(); for (const e of this.#edges.values()) { if (e.kind !== 'calls') continue; const caller = this.#nodes.get(e.source_id); @@ -450,7 +489,7 @@ export class InMemoryRepository extends Repository { return targets; } - countCrossFileCallers(nodeId, file) { + countCrossFileCallers(nodeId: number, file: string): number { let count = 0; for (const e of this.#edges.values()) { if (e.target_id === nodeId && e.kind === 'calls') { @@ -461,11 +500,11 @@ export class InMemoryRepository extends Repository { return count; } - getClassHierarchy(classNodeId) { - const ancestors = new Set(); + getClassHierarchy(classNodeId: number): Set { + const ancestors = new Set(); const queue = [classNodeId]; while (queue.length > 0) { - const current = queue.shift(); + const current = queue.shift()!; for (const e of this.#edges.values()) { if (e.source_id === current && e.kind === 'extends') { const target = this.#nodes.get(e.target_id); @@ -479,8 +518,30 @@ export class InMemoryRepository extends Repository { return ancestors; } - findIntraFileCallEdges(file) { - const results = []; + findImplementors(nodeId: number): RelatedNodeRow[] { + const results: RelatedNodeRow[] = []; + for (const e of this.#edges.values()) { + if (e.target_id === nodeId && e.kind === 'implements') { + const n = this.#nodes.get(e.source_id); + if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line }); + } + } + return results; + } + + findInterfaces(nodeId: number): RelatedNodeRow[] { + const results: RelatedNodeRow[] = []; + for (const e of this.#edges.values()) { + if (e.source_id === nodeId && e.kind === 'implements') { + const n = this.#nodes.get(e.target_id); + if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line }); + } + } + return results; + } + + findIntraFileCallEdges(file: string): IntraFileCallEdge[] { + const results: IntraFileCallEdge[] = []; for (const e of this.#edges.values()) { if (e.kind !== 'calls') continue; const caller = this.#nodes.get(e.source_id); @@ -489,7 +550,7 @@ export class InMemoryRepository extends Repository { results.push({ caller_name: caller.name, callee_name: callee.name }); } } - const lineByName = new Map(); + const lineByName = new Map(); for (const n of this.#nodes.values()) { if (n.file === file) lineByName.set(n.name, n.line); } @@ -500,25 +561,25 @@ export class InMemoryRepository extends Repository { // ── Graph-read queries ──────────────────────────────────────────── - getCallableNodes() { + getCallableNodes(): CallableNodeRow[] { return [...this.#nodes.values()] .filter((n) => CORE_SYMBOL_KINDS.includes(n.kind)) .map((n) => ({ id: n.id, name: n.name, kind: n.kind, file: n.file })); } - getCallEdges() { + getCallEdges(): CallEdgeRow[] { return [...this.#edges.values()] .filter((e) => e.kind === 'calls') .map((e) => ({ source_id: e.source_id, target_id: e.target_id, confidence: e.confidence })); } - getFileNodesAll() { + getFileNodesAll(): FileNodeRow[] { return [...this.#nodes.values()] .filter((n) => n.kind === 'file') .map((n) => ({ id: n.id, name: n.name, file: n.file })); } - getImportEdges() { + getImportEdges(): ImportGraphEdgeRow[] { return [...this.#edges.values()] .filter((e) => e.kind === 'imports' || e.kind === 'imports-type') .map((e) => ({ source_id: e.source_id, target_id: e.target_id })); @@ -526,27 +587,27 @@ export class InMemoryRepository extends Repository { // ── Optional table checks ───────────────────────────────────────── - hasCfgTables() { + hasCfgTables(): boolean { return false; } - hasEmbeddings() { + hasEmbeddings(): boolean { return false; } - hasDataflowTable() { + hasDataflowTable(): boolean { return false; } - getComplexityForNode(nodeId) { + getComplexityForNode(nodeId: number): ComplexityMetrics | undefined { return this.#complexity.get(nodeId); } // ── Private helpers ─────────────────────────────────────────────── /** Compute fan-in (incoming 'calls' edge count) for all nodes. */ - #computeFanIn() { - const fanIn = new Map(); + #computeFanIn(): Map { + const fanIn = new Map(); for (const e of this.#edges.values()) { if (e.kind === 'calls') { fanIn.set(e.target_id, (fanIn.get(e.target_id) ?? 0) + 1); @@ -556,7 +617,7 @@ export class InMemoryRepository extends Repository { } /** Internal generator for function/method/class listing with filters. */ - *#iterateFunctionNodesImpl(opts = {}) { + *#iterateFunctionNodesImpl(opts: ListFunctionOpts = {}): IterableIterator { let nodes = [...this.#nodes.values()].filter((n) => ['function', 'method', 'class'].includes(n.kind), ); @@ -589,7 +650,7 @@ export class InMemoryRepository extends Repository { line: n.line, end_line: n.end_line, role: n.role, - }; + } as NodeRow; } } } diff --git a/src/db/repository/index.js b/src/db/repository/index.ts similarity index 100% rename from src/db/repository/index.js rename to src/db/repository/index.ts diff --git a/src/db/repository/nodes.js b/src/db/repository/nodes.ts similarity index 55% rename from src/db/repository/nodes.js rename to src/db/repository/nodes.ts index ffcf6297..ae5cbcdd 100644 --- a/src/db/repository/nodes.js +++ b/src/db/repository/nodes.ts @@ -1,5 +1,16 @@ import { ConfigError } from '../../shared/errors.js'; import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js'; +import type { + BetterSqlite3Database, + ChildNodeRow, + ListFunctionOpts, + NodeIdRow, + NodeRow, + NodeRowWithFanIn, + QueryOpts, + StmtCache, + TriageQueryOpts, +} from '../../types.js'; import { buildFileConditionSQL, NodeQuery } from '../query-builder.js'; import { cachedStmt } from './cached-stmt.js'; @@ -7,14 +18,12 @@ import { cachedStmt } from './cached-stmt.js'; /** * Find nodes matching a name pattern, with fan-in count. - * @param {object} db - * @param {string} namePattern - LIKE pattern (already wrapped with %) - * @param {object} [opts] - * @param {string[]} [opts.kinds] - * @param {string} [opts.file] - * @returns {object[]} */ -export function findNodesWithFanIn(db, namePattern, opts = {}) { +export function findNodesWithFanIn( + db: BetterSqlite3Database, + namePattern: string, + opts: QueryOpts = {}, +): NodeRowWithFanIn[] { const q = new NodeQuery() .select('n.*, COALESCE(fi.cnt, 0) AS fan_in') .withFanIn() @@ -32,11 +41,11 @@ export function findNodesWithFanIn(db, namePattern, opts = {}) { /** * Fetch nodes for triage scoring: fan-in + complexity + churn. - * @param {object} db - * @param {object} [opts] - * @returns {object[]} */ -export function findNodesForTriage(db, opts = {}) { +export function findNodesForTriage( + db: BetterSqlite3Database, + opts: TriageQueryOpts = {}, +): NodeRow[] { if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) { throw new ConfigError( `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`, @@ -71,10 +80,8 @@ export function findNodesForTriage(db, opts = {}) { /** * Shared query builder for function/method/class node listing. - * @param {object} [opts] - * @returns {NodeQuery} */ -function _functionNodeQuery(opts = {}) { +function _functionNodeQuery(opts: ListFunctionOpts = {}): InstanceType { return new NodeQuery() .select('name, kind, file, line, end_line, role') .kinds(['function', 'method', 'class']) @@ -86,84 +93,74 @@ function _functionNodeQuery(opts = {}) { /** * List function/method/class nodes with basic info. - * @param {object} db - * @param {object} [opts] - * @returns {object[]} */ -export function listFunctionNodes(db, opts = {}) { +export function listFunctionNodes( + db: BetterSqlite3Database, + opts: ListFunctionOpts = {}, +): NodeRow[] { return _functionNodeQuery(opts).all(db); } /** * Iterator version of listFunctionNodes for memory efficiency. - * @param {object} db - * @param {object} [opts] - * @returns {IterableIterator} */ -export function iterateFunctionNodes(db, opts = {}) { +export function iterateFunctionNodes( + db: BetterSqlite3Database, + opts: ListFunctionOpts = {}, +): IterableIterator { return _functionNodeQuery(opts).iterate(db); } // ─── Statement caches (one prepared statement per db instance) ──────────── // WeakMap keys on the db object so statements are GC'd when the db closes. -const _countNodesStmt = new WeakMap(); -const _countEdgesStmt = new WeakMap(); -const _countFilesStmt = new WeakMap(); -const _findNodeByIdStmt = new WeakMap(); -const _findNodesByFileStmt = new WeakMap(); -const _findFileNodesStmt = new WeakMap(); -const _getNodeIdStmt = new WeakMap(); -const _getFunctionNodeIdStmt = new WeakMap(); -const _bulkNodeIdsByFileStmt = new WeakMap(); -const _findNodeChildrenStmt = new WeakMap(); -const _findNodeByQualifiedNameStmt = new WeakMap(); +const _countNodesStmt: StmtCache<{ cnt: number }> = new WeakMap(); +const _countEdgesStmt: StmtCache<{ cnt: number }> = new WeakMap(); +const _countFilesStmt: StmtCache<{ cnt: number }> = new WeakMap(); +const _findNodeByIdStmt: StmtCache = new WeakMap(); +const _findNodesByFileStmt: StmtCache = new WeakMap(); +const _findFileNodesStmt: StmtCache = new WeakMap(); +const _getNodeIdStmt: StmtCache<{ id: number }> = new WeakMap(); +const _getFunctionNodeIdStmt: StmtCache<{ id: number }> = new WeakMap(); +const _bulkNodeIdsByFileStmt: StmtCache = new WeakMap(); +const _findNodeChildrenStmt: StmtCache = new WeakMap(); +const _findNodeByQualifiedNameStmt: StmtCache = new WeakMap(); /** * Count total nodes. - * @param {object} db - * @returns {number} */ -export function countNodes(db) { - return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt; +export function countNodes(db: BetterSqlite3Database): number { + return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get()?.cnt ?? 0; } /** * Count total edges. - * @param {object} db - * @returns {number} */ -export function countEdges(db) { - return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt; +export function countEdges(db: BetterSqlite3Database): number { + return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get()?.cnt ?? 0; } /** * Count distinct files. - * @param {object} db - * @returns {number} */ -export function countFiles(db) { - return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt; +export function countFiles(db: BetterSqlite3Database): number { + return ( + cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get()?.cnt ?? 0 + ); } // ─── Shared node lookups ─────────────────────────────────────────────── /** * Find a single node by ID. - * @param {object} db - * @param {number} id - * @returns {object|undefined} */ -export function findNodeById(db, id) { +export function findNodeById(db: BetterSqlite3Database, id: number): NodeRow | undefined { return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id); } /** * Find non-file nodes for a given file path (exact match), ordered by line. - * @param {object} db - * @param {string} file - Exact file path - * @returns {object[]} */ -export function findNodesByFile(db, file) { +export function findNodesByFile(db: BetterSqlite3Database, file: string): NodeRow[] { return cachedStmt( _findNodesByFileStmt, db, @@ -173,11 +170,8 @@ export function findNodesByFile(db, file) { /** * Find file-kind nodes matching a LIKE pattern. - * @param {object} db - * @param {string} fileLike - LIKE pattern (caller wraps with %) - * @returns {object[]} */ -export function findFileNodes(db, fileLike) { +export function findFileNodes(db: BetterSqlite3Database, fileLike: string): NodeRow[] { return cachedStmt( _findFileNodesStmt, db, @@ -187,15 +181,14 @@ export function findFileNodes(db, fileLike) { /** * Look up a node's ID by its unique (name, kind, file, line) tuple. - * Shared by builder, watcher, structure, complexity, cfg, engine. - * @param {object} db - * @param {string} name - * @param {string} kind - * @param {string} file - * @param {number} line - * @returns {number|undefined} */ -export function getNodeId(db, name, kind, file, line) { +export function getNodeId( + db: BetterSqlite3Database, + name: string, + kind: string, + file: string, + line: number, +): number | undefined { return cachedStmt( _getNodeIdStmt, db, @@ -205,14 +198,13 @@ export function getNodeId(db, name, kind, file, line) { /** * Look up a function/method node's ID (kind-restricted variant of getNodeId). - * Used by complexity.js, cfg.js where only function/method kinds are expected. - * @param {object} db - * @param {string} name - * @param {string} file - * @param {number} line - * @returns {number|undefined} */ -export function getFunctionNodeId(db, name, file, line) { +export function getFunctionNodeId( + db: BetterSqlite3Database, + name: string, + file: string, + line: number, +): number | undefined { return cachedStmt( _getFunctionNodeIdStmt, db, @@ -222,13 +214,8 @@ export function getFunctionNodeId(db, name, file, line) { /** * Bulk-fetch all node IDs for a file in one query. - * Returns rows suitable for building a `name|kind|line -> id` lookup map. - * Shared by builder, ast.js, ast-analysis/engine.js. - * @param {object} db - * @param {string} file - * @returns {{ id: number, name: string, kind: string, line: number }[]} */ -export function bulkNodeIdsByFile(db, file) { +export function bulkNodeIdsByFile(db: BetterSqlite3Database, file: string): NodeIdRow[] { return cachedStmt( _bulkNodeIdsByFileStmt, db, @@ -238,11 +225,8 @@ export function bulkNodeIdsByFile(db, file) { /** * Find child nodes (parameters, properties, constants) of a parent. - * @param {object} db - * @param {number} parentId - * @returns {{ name: string, kind: string, line: number, end_line: number|null, qualified_name: string|null, scope: string|null, visibility: string|null }[]} */ -export function findNodeChildren(db, parentId) { +export function findNodeChildren(db: BetterSqlite3Database, parentId: number): ChildNodeRow[] { return cachedStmt( _findNodeChildrenStmt, db, @@ -252,17 +236,14 @@ export function findNodeChildren(db, parentId) { /** * Find all nodes that belong to a given scope (by scope column). - * Enables "all methods of class X" without traversing edges. - * @param {object} db - * @param {string} scopeName - The scope to search for (e.g., class name) - * @param {object} [opts] - * @param {string} [opts.kind] - Filter by node kind - * @param {string} [opts.file] - Filter by file path (LIKE match) - * @returns {object[]} */ -export function findNodesByScope(db, scopeName, opts = {}) { +export function findNodesByScope( + db: BetterSqlite3Database, + scopeName: string, + opts: QueryOpts = {}, +): NodeRow[] { let sql = 'SELECT * FROM nodes WHERE scope = ?'; - const params = [scopeName]; + const params: unknown[] = [scopeName]; if (opts.kind) { sql += ' AND kind = ?'; params.push(opts.kind); @@ -271,24 +252,21 @@ export function findNodesByScope(db, scopeName, opts = {}) { sql += fc.sql; params.push(...fc.params); sql += ' ORDER BY file, line'; - return db.prepare(sql).all(...params); + return db.prepare(sql).all(...params); } /** - * Find nodes by qualified name. Returns all matches since the same - * qualified_name can exist in different files (e.g., two classes named - * `DateHelper.format` in separate modules). Pass `opts.file` to narrow. - * @param {object} db - * @param {string} qualifiedName - e.g., 'DateHelper.format' - * @param {object} [opts] - * @param {string} [opts.file] - Filter by file path (LIKE match) - * @returns {object[]} + * Find nodes by qualified name. */ -export function findNodeByQualifiedName(db, qualifiedName, opts = {}) { +export function findNodeByQualifiedName( + db: BetterSqlite3Database, + qualifiedName: string, + opts: { file?: string } = {}, +): NodeRow[] { const fc = buildFileConditionSQL(opts.file, 'file'); if (fc.sql) { return db - .prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`) + .prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`) .all(qualifiedName, ...fc.params); } return cachedStmt( diff --git a/src/db/repository/sqlite-repository.js b/src/db/repository/sqlite-repository.ts similarity index 59% rename from src/db/repository/sqlite-repository.js rename to src/db/repository/sqlite-repository.ts index c6b556e9..290e1ed0 100644 --- a/src/db/repository/sqlite-repository.js +++ b/src/db/repository/sqlite-repository.ts @@ -1,3 +1,22 @@ +import type { + AdjacentEdgeRow, + BetterSqlite3Database, + CallableNodeRow, + CallEdgeRow, + ChildNodeRow, + ComplexityMetrics, + FileNodeRow, + ImportEdgeRow, + ImportGraphEdgeRow, + IntraFileCallEdge, + ListFunctionOpts, + NodeIdRow, + NodeRow, + NodeRowWithFanIn, + QueryOpts, + RelatedNodeRow, + TriageQueryOpts, +} from '../../types.js'; import { Repository } from './base.js'; import { hasCfgTables } from './cfg.js'; import { getComplexityForNode } from './complexity.js'; @@ -12,9 +31,11 @@ import { findCallers, findCrossFileCallTargets, findDistinctCallers, + findImplementors, findImportDependents, findImportSources, findImportTargets, + findInterfaces, findIntraFileCallEdges, getClassHierarchy, } from './edges.js'; @@ -44,176 +65,183 @@ import { * behind the Repository interface so callers can use `repo.method(...)`. */ export class SqliteRepository extends Repository { - #db; + #db: BetterSqlite3Database; - /** @param {object} db - better-sqlite3 Database instance */ - constructor(db) { + constructor(db: BetterSqlite3Database) { super(); this.#db = db; } /** Expose the underlying db for code that still needs raw access. */ - get db() { + get db(): BetterSqlite3Database { return this.#db; } // ── Node lookups ────────────────────────────────────────────────── - findNodeById(id) { + findNodeById(id: number): NodeRow | undefined { return findNodeById(this.#db, id); } - findNodesByFile(file) { + findNodesByFile(file: string): NodeRow[] { return findNodesByFile(this.#db, file); } - findFileNodes(fileLike) { + findFileNodes(fileLike: string): NodeRow[] { return findFileNodes(this.#db, fileLike); } - findNodesWithFanIn(namePattern, opts) { + findNodesWithFanIn(namePattern: string, opts?: QueryOpts): NodeRowWithFanIn[] { return findNodesWithFanIn(this.#db, namePattern, opts); } - countNodes() { + countNodes(): number { return countNodes(this.#db); } - countEdges() { + countEdges(): number { return countEdges(this.#db); } - countFiles() { + countFiles(): number { return countFiles(this.#db); } - getNodeId(name, kind, file, line) { + getNodeId(name: string, kind: string, file: string, line: number): number | undefined { return getNodeId(this.#db, name, kind, file, line); } - getFunctionNodeId(name, file, line) { + getFunctionNodeId(name: string, file: string, line: number): number | undefined { return getFunctionNodeId(this.#db, name, file, line); } - bulkNodeIdsByFile(file) { + bulkNodeIdsByFile(file: string): NodeIdRow[] { return bulkNodeIdsByFile(this.#db, file); } - findNodeChildren(parentId) { + findNodeChildren(parentId: number): ChildNodeRow[] { return findNodeChildren(this.#db, parentId); } - findNodesByScope(scopeName, opts) { + findNodesByScope(scopeName: string, opts?: QueryOpts): NodeRow[] { return findNodesByScope(this.#db, scopeName, opts); } - findNodeByQualifiedName(qualifiedName, opts) { + findNodeByQualifiedName(qualifiedName: string, opts?: { file?: string }): NodeRow[] { return findNodeByQualifiedName(this.#db, qualifiedName, opts); } - listFunctionNodes(opts) { + listFunctionNodes(opts?: ListFunctionOpts): NodeRow[] { return listFunctionNodes(this.#db, opts); } - iterateFunctionNodes(opts) { + iterateFunctionNodes(opts?: ListFunctionOpts): IterableIterator { return iterateFunctionNodes(this.#db, opts); } - findNodesForTriage(opts) { + findNodesForTriage(opts?: TriageQueryOpts): NodeRow[] { return findNodesForTriage(this.#db, opts); } // ── Edge queries ────────────────────────────────────────────────── - findCallees(nodeId) { + findCallees(nodeId: number): RelatedNodeRow[] { return findCallees(this.#db, nodeId); } - findCallers(nodeId) { + findCallers(nodeId: number): RelatedNodeRow[] { return findCallers(this.#db, nodeId); } - findDistinctCallers(nodeId) { + findDistinctCallers(nodeId: number): RelatedNodeRow[] { return findDistinctCallers(this.#db, nodeId); } - findAllOutgoingEdges(nodeId) { + findAllOutgoingEdges(nodeId: number): AdjacentEdgeRow[] { return findAllOutgoingEdges(this.#db, nodeId); } - findAllIncomingEdges(nodeId) { + findAllIncomingEdges(nodeId: number): AdjacentEdgeRow[] { return findAllIncomingEdges(this.#db, nodeId); } - findCalleeNames(nodeId) { + findCalleeNames(nodeId: number): string[] { return findCalleeNames(this.#db, nodeId); } - findCallerNames(nodeId) { + findCallerNames(nodeId: number): string[] { return findCallerNames(this.#db, nodeId); } - findImportTargets(nodeId) { + findImportTargets(nodeId: number): ImportEdgeRow[] { return findImportTargets(this.#db, nodeId); } - findImportSources(nodeId) { + findImportSources(nodeId: number): ImportEdgeRow[] { return findImportSources(this.#db, nodeId); } - findImportDependents(nodeId) { + findImportDependents(nodeId: number): NodeRow[] { return findImportDependents(this.#db, nodeId); } - findCrossFileCallTargets(file) { + findCrossFileCallTargets(file: string): Set { return findCrossFileCallTargets(this.#db, file); } - countCrossFileCallers(nodeId, file) { + countCrossFileCallers(nodeId: number, file: string): number { return countCrossFileCallers(this.#db, nodeId, file); } - getClassHierarchy(classNodeId) { + getClassHierarchy(classNodeId: number): Set { return getClassHierarchy(this.#db, classNodeId); } - findIntraFileCallEdges(file) { + findImplementors(nodeId: number): RelatedNodeRow[] { + return findImplementors(this.#db, nodeId); + } + + findInterfaces(nodeId: number): RelatedNodeRow[] { + return findInterfaces(this.#db, nodeId); + } + + findIntraFileCallEdges(file: string): IntraFileCallEdge[] { return findIntraFileCallEdges(this.#db, file); } // ── Graph-read queries ──────────────────────────────────────────── - getCallableNodes() { + getCallableNodes(): CallableNodeRow[] { return getCallableNodes(this.#db); } - getCallEdges() { + getCallEdges(): CallEdgeRow[] { return getCallEdges(this.#db); } - getFileNodesAll() { + getFileNodesAll(): FileNodeRow[] { return getFileNodesAll(this.#db); } - getImportEdges() { + getImportEdges(): ImportGraphEdgeRow[] { return getImportEdges(this.#db); } // ── Optional table checks ───────────────────────────────────────── - hasCfgTables() { + hasCfgTables(): boolean { return hasCfgTables(this.#db); } - hasEmbeddings() { + hasEmbeddings(): boolean { return hasEmbeddings(this.#db); } - hasDataflowTable() { + hasDataflowTable(): boolean { return hasDataflowTable(this.#db); } - getComplexityForNode(nodeId) { + getComplexityForNode(nodeId: number): ComplexityMetrics | undefined { return getComplexityForNode(this.#db, nodeId); } } diff --git a/vitest.config.js b/vitest.config.js index a92ad038..513f0ab5 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,9 +1,48 @@ +import { existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { defineConfig } from 'vitest/config'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const loaderPath = pathToFileURL(resolve(__dirname, 'scripts/ts-resolve-loader.js')).href; + +/** + * During the JS → TS migration, some .js files import from modules that have + * already been renamed to .ts. Vite only auto-resolves .js→.ts when the + * *importer* is itself a .ts file. This plugin fills the gap: when a .js + * import target doesn't exist on disk, it tries the .ts counterpart. + */ +function jsToTsResolver() { + return { + name: 'js-to-ts-resolver', + enforce: 'pre', + resolveId(source, importer) { + if (!importer || !source.endsWith('.js')) return null; + // Only handle relative/absolute paths, not bare specifiers + if (!source.startsWith('.') && !source.startsWith('/')) return null; + const importerPath = importer.startsWith('file://') + ? fileURLToPath(importer) + : importer; + const fsPath = resolve(dirname(importerPath), source); + if (!existsSync(fsPath)) { + const tsPath = fsPath.replace(/\.js$/, '.ts'); + if (existsSync(tsPath)) return tsPath; + } + return null; + }, + }; +} + export default defineConfig({ + plugins: [jsToTsResolver()], test: { globals: true, testTimeout: 30000, exclude: ['**/node_modules/**', '**/.git/**', '.claude/**'], + // Register the .js→.ts resolve loader for Node's native ESM resolver. + // This covers require() calls and child processes spawned by tests. + env: { + NODE_OPTIONS: `--experimental-strip-types --import ${loaderPath}`, + }, }, }); From 4bfc95075074892056993fb8dc2cfb89c75f9b64 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:34:22 -0600 Subject: [PATCH 03/15] feat(types): migrate core modules to TypeScript (Phase 5.4) Migrate 22 core source files from .js to .ts with full type annotations: - extractors/ (11 files): all language extractors typed with ExtractorOutput - domain/parser.ts: LANGUAGE_REGISTRY typed, parser functions annotated - domain/graph/resolve.ts: import resolution with BareSpecifier, PathAliases - domain/analysis/ (9 files): query-layer analysis modules typed All 2034 tests pass, zero type errors, biome clean. Impact: 2 functions changed, 0 affected --- src/db/repository/nodes.ts | 4 +- src/domain/analysis/{brief.js => brief.ts} | 35 +- .../analysis/{context.js => context.ts} | 178 +++++++---- .../{dependencies.js => dependencies.ts} | 120 +++++-- .../analysis/{exports.js => exports.ts} | 58 +++- src/domain/analysis/{impact.js => impact.ts} | 278 +++++++++------- ...{implementations.js => implementations.ts} | 20 +- .../analysis/{module-map.js => module-map.ts} | 147 ++++++--- src/domain/analysis/{roles.js => roles.ts} | 22 +- .../{symbol-lookup.js => symbol-lookup.ts} | 102 +++--- src/domain/graph/{resolve.js => resolve.ts} | 96 ++++-- src/domain/{parser.js => parser.ts} | 174 ++++++---- src/extractors/{csharp.js => csharp.ts} | 83 +++-- src/extractors/{go.js => go.ts} | 92 +++--- src/extractors/{hcl.js => hcl.ts} | 79 +++-- src/extractors/{helpers.js => helpers.ts} | 35 +- src/extractors/{index.js => index.ts} | 0 src/extractors/{java.js => java.ts} | 67 ++-- .../{javascript.js => javascript.ts} | 298 ++++++++++-------- src/extractors/{php.js => php.ts} | 77 +++-- src/extractors/{python.js => python.ts} | 99 +++--- src/extractors/{ruby.js => ruby.ts} | 65 ++-- src/extractors/{rust.js => rust.ts} | 72 +++-- src/types.ts | 2 + 24 files changed, 1379 insertions(+), 824 deletions(-) rename src/domain/analysis/{brief.js => brief.ts} (83%) rename src/domain/analysis/{context.js => context.ts} (71%) rename src/domain/analysis/{dependencies.js => dependencies.ts} (76%) rename src/domain/analysis/{exports.js => exports.ts} (77%) rename src/domain/analysis/{impact.js => impact.ts} (77%) rename src/domain/analysis/{implementations.js => implementations.ts} (80%) rename src/domain/analysis/{module-map.js => module-map.ts} (68%) rename src/domain/analysis/{roles.js => roles.ts} (74%) rename src/domain/analysis/{symbol-lookup.js => symbol-lookup.ts} (65%) rename src/domain/graph/{resolve.js => resolve.ts} (83%) rename src/domain/{parser.js => parser.ts} (82%) rename src/extractors/{csharp.js => csharp.ts} (82%) rename src/extractors/{go.js => go.ts} (82%) rename src/extractors/{hcl.js => hcl.ts} (51%) rename src/extractors/{helpers.js => helpers.ts} (61%) rename src/extractors/{index.js => index.ts} (100%) rename src/extractors/{java.js => java.ts} (82%) rename src/extractors/{javascript.js => javascript.ts} (80%) rename src/extractors/{php.js => php.ts} (80%) rename src/extractors/{python.js => python.ts} (80%) rename src/extractors/{ruby.js => ruby.ts} (80%) rename src/extractors/{rust.js => rust.ts} (80%) diff --git a/src/db/repository/nodes.ts b/src/db/repository/nodes.ts index ae5cbcdd..1b0a58ff 100644 --- a/src/db/repository/nodes.ts +++ b/src/db/repository/nodes.ts @@ -248,7 +248,7 @@ export function findNodesByScope( sql += ' AND kind = ?'; params.push(opts.kind); } - const fc = buildFileConditionSQL(opts.file, 'file'); + const fc = buildFileConditionSQL(opts.file ?? '', 'file'); sql += fc.sql; params.push(...fc.params); sql += ' ORDER BY file, line'; @@ -263,7 +263,7 @@ export function findNodeByQualifiedName( qualifiedName: string, opts: { file?: string } = {}, ): NodeRow[] { - const fc = buildFileConditionSQL(opts.file, 'file'); + const fc = buildFileConditionSQL(opts.file ?? '', 'file'); if (fc.sql) { return db .prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`) diff --git a/src/domain/analysis/brief.js b/src/domain/analysis/brief.ts similarity index 83% rename from src/domain/analysis/brief.js rename to src/domain/analysis/brief.ts index 78c3d342..e88463d2 100644 --- a/src/domain/analysis/brief.js +++ b/src/domain/analysis/brief.ts @@ -26,10 +26,12 @@ const BRIEF_KINDS = new Set([ /** * Compute file risk tier from symbol roles and max fan-in. - * @param {{ role: string|null, callerCount: number }[]} symbols - * @returns {'high'|'medium'|'low'} */ -function computeRiskTier(symbols, highThreshold = 10, mediumThreshold = 3) { +function computeRiskTier( + symbols: Array<{ role: string | null; callerCount: number }>, + highThreshold = 10, + mediumThreshold = 3, +): 'high' | 'medium' | 'low' { let maxCallers = 0; let hasCoreRole = false; for (const s of symbols) { @@ -45,12 +47,13 @@ function computeRiskTier(symbols, highThreshold = 10, mediumThreshold = 3) { * BFS to count transitive callers for a single node. * Lightweight variant — only counts, does not collect details. */ -function countTransitiveCallers(db, startId, noTests, maxDepth = 5) { +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function countTransitiveCallers(db: any, startId: number, noTests: boolean, maxDepth = 5): number { const visited = new Set([startId]); let frontier = [startId]; for (let d = 1; d <= maxDepth; d++) { - const nextFrontier = []; + const nextFrontier: number[] = []; for (const fid of frontier) { const callers = findDistinctCallers(db, fid); for (const c of callers) { @@ -71,12 +74,18 @@ function countTransitiveCallers(db, startId, noTests, maxDepth = 5) { * Count transitive file-level import dependents via BFS. * Depth-bounded to match countTransitiveCallers and keep hook latency predictable. */ -function countTransitiveImporters(db, fileNodeIds, noTests, maxDepth = 5) { +function countTransitiveImporters( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + fileNodeIds: number[], + noTests: boolean, + maxDepth = 5, +): number { const visited = new Set(fileNodeIds); let frontier = [...fileNodeIds]; for (let d = 1; d <= maxDepth; d++) { - const nextFrontier = []; + const nextFrontier: number[] = []; for (const current of frontier) { const dependents = findImportDependents(db, current); for (const dep of dependents) { @@ -96,13 +105,13 @@ function countTransitiveImporters(db, fileNodeIds, noTests, maxDepth = 5) { /** * Produce a token-efficient file brief: symbols with roles and caller counts, * importer info with transitive count, and file risk tier. - * - * @param {string} file - File path (partial match) - * @param {string} customDbPath - Path to graph.db - * @param {{ noTests?: boolean }} opts - * @returns {{ file: string, results: object[] }} */ -export function briefData(file, customDbPath, opts = {}) { +export function briefData( + file: string, + customDbPath: string | undefined, + // biome-ignore lint/suspicious/noExplicitAny: config shape not yet typed + opts: { noTests?: boolean; config?: any } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; diff --git a/src/domain/analysis/context.js b/src/domain/analysis/context.ts similarity index 71% rename from src/domain/analysis/context.js rename to src/domain/analysis/context.ts index 1f5f113d..62c0284c 100644 --- a/src/domain/analysis/context.js +++ b/src/domain/analysis/context.ts @@ -30,15 +30,21 @@ import { normalizeSymbol } from '../../shared/normalize.js'; import { paginateResult } from '../../shared/paginate.js'; import { findMatchingNodes } from './symbol-lookup.js'; -function buildCallees(db, node, repoRoot, getFileLines, opts) { +function buildCallees( + db: any, + node: any, + repoRoot: string, + getFileLines: (file: string) => string[] | null, + opts: { noTests: boolean; depth: number; displayOpts: Record }, +): any[] { const { noTests, depth, displayOpts } = opts; const calleeRows = findCallees(db, node.id); - const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows; + const filteredCallees = noTests ? calleeRows.filter((c: any) => !isTestFile(c.file)) : calleeRows; - const callees = filteredCallees.map((c) => { + const callees = filteredCallees.map((c: any) => { const cLines = getFileLines(c.file); const summary = cLines ? extractSummary(cLines, c.line, displayOpts) : null; - let calleeSource = null; + let calleeSource: string | null = null; if (depth >= 1) { calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line, displayOpts); } @@ -54,12 +60,12 @@ function buildCallees(db, node, repoRoot, getFileLines, opts) { }); if (depth > 1) { - const visited = new Set(filteredCallees.map((c) => c.id)); + const visited = new Set(filteredCallees.map((c: any) => c.id)); visited.add(node.id); - let frontier = filteredCallees.map((c) => c.id); + let frontier = filteredCallees.map((c: any) => c.id); const maxDepth = Math.min(depth, 5); for (let d = 2; d <= maxDepth; d++) { - const nextFrontier = []; + const nextFrontier: number[] = []; for (const fid of frontier) { const deeper = findCallees(db, fid); for (const c of deeper) { @@ -87,7 +93,7 @@ function buildCallees(db, node, repoRoot, getFileLines, opts) { return callees; } -function buildCallers(db, node, noTests) { +function buildCallers(db: any, node: any, noTests: boolean): any[] { let callerRows = findCallers(db, node.id); if (node.kind === 'method' && node.name.includes('.')) { @@ -96,12 +102,12 @@ function buildCallers(db, node, noTests) { for (const rm of relatedMethods) { if (rm.id === node.id) continue; const extraCallers = findCallers(db, rm.id); - callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name }))); + callerRows.push(...extraCallers.map((c: any) => ({ ...c, viaHierarchy: rm.name }))); } } - if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file)); + if (noTests) callerRows = callerRows.filter((c: any) => !isTestFile(c.file)); - return callerRows.map((c) => ({ + return callerRows.map((c: any) => ({ name: c.name, kind: c.kind, file: c.file, @@ -113,46 +119,61 @@ function buildCallers(db, node, noTests) { const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']); const IMPLEMENTOR_KINDS = new Set(['class', 'struct', 'record', 'enum']); -function buildImplementationInfo(db, node, noTests) { +function buildImplementationInfo(db: any, node: any, noTests: boolean): object { // For interfaces/traits: show who implements them if (INTERFACE_LIKE_KINDS.has(node.kind)) { let impls = findImplementors(db, node.id); - if (noTests) impls = impls.filter((n) => !isTestFile(n.file)); + if (noTests) impls = impls.filter((n: any) => !isTestFile(n.file)); return { - implementors: impls.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })), + implementors: impls.map((n: any) => ({ + name: n.name, + kind: n.kind, + file: n.file, + line: n.line, + })), }; } // For classes/structs: show what they implement if (IMPLEMENTOR_KINDS.has(node.kind)) { let ifaces = findInterfaces(db, node.id); - if (noTests) ifaces = ifaces.filter((n) => !isTestFile(n.file)); + if (noTests) ifaces = ifaces.filter((n: any) => !isTestFile(n.file)); if (ifaces.length > 0) { return { - implements: ifaces.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })), + implements: ifaces.map((n: any) => ({ + name: n.name, + kind: n.kind, + file: n.file, + line: n.line, + })), }; } } return {}; } -function buildRelatedTests(db, node, getFileLines, includeTests) { +function buildRelatedTests( + db: any, + node: any, + getFileLines: (file: string) => string[] | null, + includeTests: boolean, +): any[] { const testCallerRows = findCallers(db, node.id); - const testCallers = testCallerRows.filter((c) => isTestFile(c.file)); + const testCallers = testCallerRows.filter((c: any) => isTestFile(c.file)); - const testsByFile = new Map(); + const testsByFile = new Map(); for (const tc of testCallers) { if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []); - testsByFile.get(tc.file).push(tc); + testsByFile.get(tc.file)?.push(tc); } - const relatedTests = []; + const relatedTests: any[] = []; for (const [file] of testsByFile) { const tLines = getFileLines(file); - const testNames = []; + const testNames: string[] = []; if (tLines) { for (const tl of tLines) { const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/); - if (tm) testNames.push(tm[1]); + if (tm && tm[1]) testNames.push(tm[1]); } } const testSource = includeTests && tLines ? tLines.join('\n') : undefined; @@ -167,7 +188,7 @@ function buildRelatedTests(db, node, getFileLines, includeTests) { return relatedTests; } -function getComplexityMetrics(db, nodeId) { +function getComplexityMetrics(db: any, nodeId: number): object | null { try { const cRow = getComplexityForNode(db, nodeId); if (!cRow) return null; @@ -178,38 +199,43 @@ function getComplexityMetrics(db, nodeId) { maintainabilityIndex: cRow.maintainability_index || 0, halsteadVolume: cRow.halstead_volume || 0, }; - } catch (e) { + } catch (e: any) { debug(`complexity lookup failed for node ${nodeId}: ${e.message}`); return null; } } -function getNodeChildrenSafe(db, nodeId) { +function getNodeChildrenSafe(db: any, nodeId: number): any[] { try { - return findNodeChildren(db, nodeId).map((c) => ({ + return findNodeChildren(db, nodeId).map((c: any) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null, })); - } catch (e) { + } catch (e: any) { debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`); return []; } } -function explainFileImpl(db, target, getFileLines, displayOpts) { +function explainFileImpl( + db: any, + target: string, + getFileLines: (file: string) => string[] | null, + displayOpts: Record, +): any[] { const fileNodes = findFileNodes(db, `%${target}%`); if (fileNodes.length === 0) return []; - return fileNodes.map((fn) => { + return fileNodes.map((fn: any) => { const symbols = findNodesByFile(db, fn.file); // IDs of symbols that have incoming calls from other files (public) const publicIds = findCrossFileCallTargets(db, fn.file); const fileLines = getFileLines(fn.file); - const mapSymbol = (s) => ({ + const mapSymbol = (s: any) => ({ name: s.name, kind: s.kind, line: s.line, @@ -218,17 +244,17 @@ function explainFileImpl(db, target, getFileLines, displayOpts) { signature: fileLines ? extractSignature(fileLines, s.line, displayOpts) : null, }); - const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol); - const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol); + const publicApi = symbols.filter((s: any) => publicIds.has(s.id)).map(mapSymbol); + const internal = symbols.filter((s: any) => !publicIds.has(s.id)).map(mapSymbol); - const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file })); - const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file })); + const imports = findImportTargets(db, fn.id).map((r: any) => ({ file: r.file })); + const importedBy = findImportSources(db, fn.id).map((r: any) => ({ file: r.file })); const intraEdges = findIntraFileCallEdges(db, fn.file); - const dataFlowMap = new Map(); + const dataFlowMap = new Map(); for (const edge of intraEdges) { if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []); - dataFlowMap.get(edge.caller_name).push(edge.callee_name); + dataFlowMap.get(edge.caller_name)?.push(edge.callee_name); } const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({ caller, @@ -237,12 +263,12 @@ function explainFileImpl(db, target, getFileLines, displayOpts) { const metric = db .prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`) - .get(fn.id); + .get(fn.id) as any; let lineCount = metric?.line_count || null; if (!lineCount) { const maxLine = db .prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`) - .get(fn.file); + .get(fn.file) as any; lineCount = maxLine?.max_end || null; } @@ -259,42 +285,48 @@ function explainFileImpl(db, target, getFileLines, displayOpts) { }); } -function explainFunctionImpl(db, target, noTests, getFileLines, displayOpts) { +function explainFunctionImpl( + db: any, + target: string, + noTests: boolean, + getFileLines: (file: string) => string[] | null, + displayOpts: Record, +): any[] { let nodes = db .prepare( `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`, ) - .all(`%${target}%`); - if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file)); + .all(`%${target}%`) as any[]; + if (noTests) nodes = nodes.filter((n: any) => !isTestFile(n.file)); if (nodes.length === 0) return []; const hc = new Map(); - return nodes.slice(0, 10).map((node) => { + return nodes.slice(0, 10).map((node: any) => { const fileLines = getFileLines(node.file); const lineCount = node.end_line ? node.end_line - node.line + 1 : null; const summary = fileLines ? extractSummary(fileLines, node.line, displayOpts) : null; const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null; - const callees = findCallees(db, node.id).map((c) => ({ + const callees = findCallees(db, node.id).map((c: any) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, })); - let callers = findCallers(db, node.id).map((c) => ({ + let callers = findCallers(db, node.id).map((c: any) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, })); - if (noTests) callers = callers.filter((c) => !isTestFile(c.file)); + if (noTests) callers = callers.filter((c: any) => !isTestFile(c.file)); const testCallerRows = findCallers(db, node.id); - const seenFiles = new Set(); + const seenFiles = new Set(); const relatedTests = testCallerRows - .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file)) - .map((r) => ({ file: r.file })); + .filter((r: any) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file)) + .map((r: any) => ({ file: r.file })); return { ...normalizeSymbol(node, db, hc), @@ -310,17 +342,17 @@ function explainFunctionImpl(db, target, noTests, getFileLines, displayOpts) { } function explainCallees( - parentResults, - currentDepth, - visited, - db, - noTests, - getFileLines, - displayOpts, -) { + parentResults: any[], + currentDepth: number, + visited: Set, + db: any, + noTests: boolean, + getFileLines: (file: string) => string[] | null, + displayOpts: Record, +): void { if (currentDepth <= 0) return; for (const r of parentResults) { - const newCallees = []; + const newCallees: any[] = []; for (const callee of r.callees) { const key = `${callee.name}:${callee.file}:${callee.line}`; if (visited.has(key)) continue; @@ -332,7 +364,9 @@ function explainCallees( getFileLines, displayOpts, ); - const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line); + const exact = calleeResults.find( + (cr: any) => cr.file === callee.file && cr.line === callee.line, + ); if (exact) { exact._depth = (r._depth || 0) + 1; newCallees.push(exact); @@ -347,7 +381,21 @@ function explainCallees( // ─── Exported functions ────────────────────────────────────────────────── -export function contextData(name, customDbPath, opts = {}) { +export function contextData( + name: string, + customDbPath: string | undefined, + opts: { + depth?: number; + noSource?: boolean; + noTests?: boolean; + includeTests?: boolean; + config?: any; + file?: string; + kind?: string; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const depth = opts.depth || 0; @@ -368,7 +416,7 @@ export function contextData(name, customDbPath, opts = {}) { const getFileLines = createFileLinesReader(repoRoot); - const results = nodes.map((node) => { + const results = nodes.map((node: any) => { const fileLines = getFileLines(node.file); const source = noSource @@ -413,7 +461,11 @@ export function contextData(name, customDbPath, opts = {}) { } } -export function explainData(target, customDbPath, opts = {}) { +export function explainData( + target: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; depth?: number; config?: any; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -434,7 +486,7 @@ export function explainData(target, customDbPath, opts = {}) { : explainFunctionImpl(db, target, noTests, getFileLines, displayOpts); if (kind === 'function' && depth > 0 && results.length > 0) { - const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`)); + const visited = new Set(results.map((r: any) => `${r.name}:${r.file}:${r.line}`)); explainCallees(results, depth, visited, db, noTests, getFileLines, displayOpts); } diff --git a/src/domain/analysis/dependencies.js b/src/domain/analysis/dependencies.ts similarity index 76% rename from src/domain/analysis/dependencies.js rename to src/domain/analysis/dependencies.ts index 867cd5bd..3f251a9f 100644 --- a/src/domain/analysis/dependencies.js +++ b/src/domain/analysis/dependencies.ts @@ -13,7 +13,11 @@ import { normalizeSymbol } from '../../shared/normalize.js'; import { paginateResult } from '../../shared/paginate.js'; import { findMatchingNodes } from './symbol-lookup.js'; -export function fileDepsData(file, customDbPath, opts = {}) { +export function fileDepsData( + file: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -33,7 +37,10 @@ export function fileDepsData(file, customDbPath, opts = {}) { return { file: fn.file, - imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })), + imports: importsTo.map((i) => ({ + file: i.file, + typeOnly: i.edge_kind === 'imports-type', + })), importedBy: importedBy.map((i) => ({ file: i.file })), definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })), }; @@ -50,22 +57,35 @@ export function fileDepsData(file, customDbPath, opts = {}) { * BFS transitive caller traversal starting from `callers` of `nodeId`. * Returns an object keyed by depth (2..depth) → array of caller descriptors. */ -function buildTransitiveCallers(db, callers, nodeId, depth, noTests) { - const transitiveCallers = {}; +function buildTransitiveCallers( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + // biome-ignore lint/suspicious/noExplicitAny: caller row shape varies + callers: any[], + nodeId: number, + depth: number, + noTests: boolean, + // biome-ignore lint/suspicious/noExplicitAny: caller row shape varies +): Record { + // biome-ignore lint/suspicious/noExplicitAny: caller row shape varies + const transitiveCallers: Record = {}; if (depth <= 1) return transitiveCallers; const visited = new Set([nodeId]); let frontier = callers .map((c) => { + // biome-ignore lint/suspicious/noExplicitAny: DB row type const row = db .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?') - .get(c.name, c.kind, c.file, c.line); + .get(c.name, c.kind, c.file, c.line) as any; return row ? { ...c, id: row.id } : null; }) - .filter(Boolean); + // biome-ignore lint/suspicious/noExplicitAny: filtering nulls + .filter(Boolean) as any[]; for (let d = 2; d <= depth; d++) { - const nextFrontier = []; + // biome-ignore lint/suspicious/noExplicitAny: caller row shape varies + const nextFrontier: any[] = []; for (const f of frontier) { if (visited.has(f.id)) continue; visited.add(f.id); @@ -75,7 +95,8 @@ function buildTransitiveCallers(db, callers, nodeId, depth, noTests) { FROM edges e JOIN nodes n ON e.source_id = n.id WHERE e.target_id = ? AND e.kind = 'calls' `) - .all(f.id); + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + .all(f.id) as any[]; for (const u of upstream) { if (noTests && isTestFile(u.file)) continue; const uid = db @@ -101,7 +122,18 @@ function buildTransitiveCallers(db, callers, nodeId, depth, noTests) { return transitiveCallers; } -export function fnDepsData(name, customDbPath, opts = {}) { +export function fnDepsData( + name: string, + customDbPath: string | undefined, + opts: { + noTests?: boolean; + file?: string; + kind?: string; + depth?: number; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const depth = opts.depth || 3; @@ -145,7 +177,7 @@ export function fnDepsData(name, customDbPath, opts = {}) { kind: c.kind, file: c.file, line: c.line, - viaHierarchy: c.viaHierarchy || undefined, + viaHierarchy: 'viaHierarchy' in c ? (c.viaHierarchy as string) : undefined, })), transitiveCallers, }; @@ -163,7 +195,23 @@ export function fnDepsData(name, customDbPath, opts = {}) { * Returns { sourceNode, targetNode, fromCandidates, toCandidates } on success, * or { earlyResult } when a caller-facing error/not-found response should be returned immediately. */ -function resolveEndpoints(db, from, to, opts) { +function resolveEndpoints( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + from: string, + to: string, + opts: { noTests?: boolean; fromFile?: string; toFile?: string; kind?: string }, +): { + // biome-ignore lint/suspicious/noExplicitAny: node row shape varies + sourceNode?: any; + // biome-ignore lint/suspicious/noExplicitAny: node row shape varies + targetNode?: any; + // biome-ignore lint/suspicious/noExplicitAny: node row shape varies + fromCandidates?: any[]; + // biome-ignore lint/suspicious/noExplicitAny: node row shape varies + toCandidates?: any[]; + earlyResult?: object; +} { const { noTests = false } = opts; const fromNodes = findMatchingNodes(db, from, { @@ -224,7 +272,20 @@ function resolveEndpoints(db, from, to, opts) { * Returns { found, parent, alternateCount, foundDepth }. * `parent` maps nodeId → { parentId, edgeKind }. */ -function bfsShortestPath(db, sourceId, targetId, edgeKinds, reverse, maxDepth, noTests) { +function bfsShortestPath( + db: any, + sourceId: number, + targetId: number, + edgeKinds: string[], + reverse: boolean, + maxDepth: number, + noTests: boolean, +): { + found: boolean; + parent: Map; + alternateCount: number; + foundDepth: number; +} { const kindPlaceholders = edgeKinds.map(() => '?').join(', '); // Forward: source_id → target_id (A calls... calls B) @@ -239,16 +300,16 @@ function bfsShortestPath(db, sourceId, targetId, edgeKinds, reverse, maxDepth, n const neighborStmt = db.prepare(neighborQuery); const visited = new Set([sourceId]); - const parent = new Map(); + const parent = new Map(); let queue = [sourceId]; let found = false; let alternateCount = 0; let foundDepth = -1; for (let depth = 1; depth <= maxDepth; depth++) { - const nextQueue = []; + const nextQueue: number[] = []; for (const currentId of queue) { - const neighbors = neighborStmt.all(currentId, ...edgeKinds); + const neighbors = neighborStmt.all(currentId, ...edgeKinds) as any[]; for (const n of neighbors) { if (noTests && isTestFile(n.file)) continue; if (n.id === targetId) { @@ -279,9 +340,13 @@ function bfsShortestPath(db, sourceId, targetId, edgeKinds, reverse, maxDepth, n * Walk the parent map from targetId back to sourceId and return an ordered * array of node IDs source → target. */ -function reconstructPath(db, pathIds, parent) { - const nodeCache = new Map(); - const getNode = (id) => { +function reconstructPath( + db: any, + pathIds: number[], + parent: Map, +): any[] { + const nodeCache = new Map(); + const getNode = (id: number) => { if (nodeCache.has(id)) return nodeCache.get(id); const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id); nodeCache.set(id, row); @@ -290,12 +355,25 @@ function reconstructPath(db, pathIds, parent) { return pathIds.map((id, idx) => { const node = getNode(id); - const edgeKind = idx === 0 ? null : parent.get(id).edgeKind; + const edgeKind = idx === 0 ? null : parent.get(id)?.edgeKind; return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind }; }); } -export function pathData(from, to, customDbPath, opts = {}) { +export function pathData( + from: string, + to: string, + customDbPath: string | undefined, + opts: { + noTests?: boolean; + maxDepth?: number; + edgeKinds?: string[]; + reverse?: boolean; + fromFile?: string; + toFile?: string; + kind?: string; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -368,7 +446,7 @@ export function pathData(from, to, customDbPath, opts = {}) { const pathIds = [targetNode.id]; let cur = targetNode.id; while (cur !== sourceNode.id) { - const p = parent.get(cur); + const p = parent.get(cur)!; pathIds.push(p.parentId); cur = p.parentId; } diff --git a/src/domain/analysis/exports.js b/src/domain/analysis/exports.ts similarity index 77% rename from src/domain/analysis/exports.js rename to src/domain/analysis/exports.ts index 629ae792..92e74099 100644 --- a/src/domain/analysis/exports.js +++ b/src/domain/analysis/exports.ts @@ -16,7 +16,12 @@ import { } from '../../shared/file-utils.js'; import { paginateResult } from '../../shared/paginate.js'; -export function exportsData(file, customDbPath, opts = {}) { +export function exportsData( + file: string, + customDbPath: string | undefined, + // biome-ignore lint/suspicious/noExplicitAny: config shape not yet typed + opts: { noTests?: boolean; config?: any; unused?: boolean; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -63,7 +68,11 @@ export function exportsData(file, customDbPath, opts = {}) { totalReexported: first.totalReexported, totalReexportedUnused: first.totalReexportedUnused, }; - const paginated = paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset }); + // biome-ignore lint/suspicious/noExplicitAny: dynamic pagination shape + const paginated: any = paginateResult(base, 'results', { + limit: opts.limit, + offset: opts.offset, + }); // Paginate reexportedSymbols with the same limit/offset (match paginateResult behaviour) if (opts.limit != null) { const off = opts.offset || 0; @@ -83,7 +92,17 @@ export function exportsData(file, customDbPath, opts = {}) { } } -function exportsFileImpl(db, target, noTests, getFileLines, unused, displayOpts) { +function exportsFileImpl( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + target: string, + noTests: boolean, + getFileLines: (file: string) => string[] | null, + unused: boolean, + // biome-ignore lint/suspicious/noExplicitAny: display config shape not yet typed + displayOpts: Record, + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated +): any[] { const fileNodes = findFileNodes(db, `%${target}%`); if (fileNodes.length === 0) return []; @@ -92,14 +111,15 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused, displayOpts) try { db.prepare('SELECT exported FROM nodes LIMIT 0').raw(); hasExportedCol = true; - } catch (e) { - debug(`exported column not available, using fallback: ${e.message}`); + } catch (e: unknown) { + debug(`exported column not available, using fallback: ${e instanceof Error ? e.message : e}`); } return fileNodes.map((fn) => { const symbols = findNodesByFile(db, fn.file); - let exported; + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + let exported: any[]; if (hasExportedCol) { // Use the exported column populated during build exported = db @@ -114,13 +134,15 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused, displayOpts) } const internalCount = symbols.length - exported.length; - const buildSymbolResult = (s, fileLines) => { + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + const buildSymbolResult = (s: any, fileLines: string[] | null) => { let consumers = db .prepare( `SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id WHERE e.target_id = ? AND e.kind = 'calls'`, ) - .all(s.id); + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + .all(s.id) as any[]; if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file)); return { @@ -141,13 +163,15 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused, displayOpts) const totalUnused = results.filter((r) => r.consumerCount === 0).length; // Files that re-export this file (barrel → this file) - const reexports = db - .prepare( - `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id + const reexports = ( + db + .prepare( + `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id WHERE e.target_id = ? AND e.kind = 'reexports'`, - ) - .all(fn.id) - .map((r) => ({ file: r.file })); + ) + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + .all(fn.id) as any[] + ).map((r) => ({ file: r.file })); // For barrel files: gather symbols re-exported from target modules const reexportTargets = db @@ -157,9 +181,11 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused, displayOpts) ) .all(fn.id); - const reexportedSymbols = []; + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + const reexportedSymbols: any[] = []; for (const target of reexportTargets) { - let targetExported; + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + let targetExported: any[]; if (hasExportedCol) { targetExported = db .prepare( diff --git a/src/domain/analysis/impact.js b/src/domain/analysis/impact.ts similarity index 77% rename from src/domain/analysis/impact.js rename to src/domain/analysis/impact.ts index 2ce1dbbf..8b822409 100644 --- a/src/domain/analysis/impact.js +++ b/src/domain/analysis/impact.ts @@ -28,9 +28,9 @@ const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']); * Check whether the graph contains any 'implements' edges. * Cached per db handle so the query runs at most once per connection. */ -const _hasImplementsCache = new WeakMap(); -function hasImplementsEdges(db) { - if (_hasImplementsCache.has(db)) return _hasImplementsCache.get(db); +const _hasImplementsCache = new WeakMap(); +function hasImplementsEdges(db: any): boolean { + if (_hasImplementsCache.has(db)) return _hasImplementsCache.get(db)!; const row = db.prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1").get(); const result = !!row; _hasImplementsCache.set(db, result); @@ -42,27 +42,32 @@ function hasImplementsEdges(db) { * When an interface/trait node is encountered (either as the start node or * during traversal), its concrete implementors are also added to the frontier * so that changes to an interface signature propagate to all implementors. - * - * @param {import('better-sqlite3').Database} db - Open read-only SQLite database handle (not a Repository) - * @param {number} startId - Starting node ID - * @param {{ noTests?: boolean, maxDepth?: number, includeImplementors?: boolean, onVisit?: (caller: object, parentId: number, depth: number) => void }} options - * @returns {{ totalDependents: number, levels: Record> }} */ export function bfsTransitiveCallers( - db, - startId, - { noTests = false, maxDepth = 3, includeImplementors = true, onVisit } = {}, -) { + db: any, + startId: number, + { + noTests = false, + maxDepth = 3, + includeImplementors = true, + onVisit, + }: { + noTests?: boolean; + maxDepth?: number; + includeImplementors?: boolean; + onVisit?: (caller: any, parentId: number, depth: number) => void; + } = {}, +): { totalDependents: number; levels: Record } { // Skip all implementor lookups when the graph has no implements edges const resolveImplementors = includeImplementors && hasImplementsEdges(db); const visited = new Set([startId]); - const levels = {}; + const levels: Record = {}; let frontier = [startId]; // Seed: if start node is an interface/trait, include its implementors at depth 1. // Implementors go into a separate list so their callers appear at depth 2, not depth 1. - const implNextFrontier = []; + const implNextFrontier: number[] = []; if (resolveImplementors) { const startNode = findNodeById(db, startId); if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) { @@ -90,15 +95,15 @@ export function bfsTransitiveCallers( if (d === 1 && implNextFrontier.length > 0) { frontier = [...frontier, ...implNextFrontier]; } - const nextFrontier = []; + const nextFrontier: number[] = []; for (const fid of frontier) { const callers = findDistinctCallers(db, fid); for (const c of callers) { if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) { visited.add(c.id); nextFrontier.push(c.id); - if (!levels[d]) levels[d] = []; - levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line }); + levels[d] = levels[d] || []; + levels[d]!.push({ name: c.name, kind: c.kind, file: c.file, line: c.line }); if (onVisit) onVisit(c, fid, d); } @@ -132,7 +137,17 @@ export function bfsTransitiveCallers( return { totalDependents: visited.size - 1, levels }; } -export function impactAnalysisData(file, customDbPath, opts = {}) { +export function impactAnalysisData( + file: string, + customDbPath: string | undefined, + opts: { + noTests?: boolean; + maxDepth?: number; + config?: any; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -141,9 +156,9 @@ export function impactAnalysisData(file, customDbPath, opts = {}) { return { file, sources: [], levels: {}, totalDependents: 0 }; } - const visited = new Set(); - const queue = []; - const levels = new Map(); + const visited = new Set(); + const queue: number[] = []; + const levels = new Map(); for (const fn of fileNodes) { visited.add(fn.id); @@ -152,8 +167,8 @@ export function impactAnalysisData(file, customDbPath, opts = {}) { } while (queue.length > 0) { - const current = queue.shift(); - const level = levels.get(current); + const current = queue.shift()!; + const level = levels.get(current)!; const dependents = findImportDependents(db, current); for (const dep of dependents) { if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) { @@ -164,7 +179,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) { } } - const byLevel = {}; + const byLevel: Record = {}; for (const [id, level] of levels) { if (level === 0) continue; if (!byLevel[level]) byLevel[level] = []; @@ -174,7 +189,7 @@ export function impactAnalysisData(file, customDbPath, opts = {}) { return { file, - sources: fileNodes.map((f) => f.file), + sources: fileNodes.map((f: any) => f.file), levels: byLevel, totalDependents: visited.size - fileNodes.length, }; @@ -183,7 +198,20 @@ export function impactAnalysisData(file, customDbPath, opts = {}) { } } -export function fnImpactData(name, customDbPath, opts = {}) { +export function fnImpactData( + name: string, + customDbPath: string | undefined, + opts: { + noTests?: boolean; + depth?: number; + config?: any; + file?: string; + kind?: string; + includeImplementors?: boolean; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const config = opts.config || loadConfig(); @@ -198,7 +226,7 @@ export function fnImpactData(name, customDbPath, opts = {}) { const includeImplementors = opts.includeImplementors !== false; - const results = nodes.map((node) => { + const results = nodes.map((node: any) => { const { levels, totalDependents } = bfsTransitiveCallers(db, node.id, { noTests, maxDepth, @@ -223,11 +251,8 @@ export function fnImpactData(name, customDbPath, opts = {}) { /** * Walk up from repoRoot until a .git directory is found. * Returns true if a git root exists, false otherwise. - * - * @param {string} repoRoot - * @returns {boolean} */ -function findGitRoot(repoRoot) { +function findGitRoot(repoRoot: string): boolean { let checkDir = repoRoot; while (checkDir) { if (fs.existsSync(path.join(checkDir, '.git'))) { @@ -243,12 +268,11 @@ function findGitRoot(repoRoot) { /** * Execute git diff and return the raw output string. * Returns `{ output: string }` on success or `{ error: string }` on failure. - * - * @param {string} repoRoot - * @param {{ staged?: boolean, ref?: string }} opts - * @returns {{ output: string } | { error: string }} */ -function runGitDiff(repoRoot, opts) { +function runGitDiff( + repoRoot: string, + opts: { staged?: boolean; ref?: string }, +): { output?: string; error?: string } { try { const args = opts.staged ? ['diff', '--cached', '--unified=0', '--no-color'] @@ -260,21 +284,21 @@ function runGitDiff(repoRoot, opts) { stdio: ['pipe', 'pipe', 'pipe'], }); return { output }; - } catch (e) { + } catch (e: any) { return { error: `Failed to run git diff: ${e.message}` }; } } /** * Parse raw git diff output into a changedRanges map and newFiles set. - * - * @param {string} diffOutput - * @returns {{ changedRanges: Map>, newFiles: Set }} */ -function parseGitDiff(diffOutput) { - const changedRanges = new Map(); - const newFiles = new Set(); - let currentFile = null; +function parseGitDiff(diffOutput: string): { + changedRanges: Map>; + newFiles: Set; +} { + const changedRanges = new Map>(); + const newFiles = new Set(); + let currentFile: string | null = null; let prevIsDevNull = false; for (const line of diffOutput.split('\n')) { @@ -288,17 +312,17 @@ function parseGitDiff(diffOutput) { } const fileMatch = line.match(/^\+\+\+ b\/(.+)/); if (fileMatch) { - currentFile = fileMatch[1]; - if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []); - if (prevIsDevNull) newFiles.add(currentFile); + currentFile = fileMatch[1] ?? null; + if (currentFile && !changedRanges.has(currentFile)) changedRanges.set(currentFile, []); + if (currentFile && prevIsDevNull) newFiles.add(currentFile); prevIsDevNull = false; continue; } const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/); if (hunkMatch && currentFile) { - const start = parseInt(hunkMatch[1], 10); + const start = parseInt(hunkMatch[1]!, 10); const count = parseInt(hunkMatch[2] || '1', 10); - changedRanges.get(currentFile).push({ start, end: start + count - 1 }); + changedRanges.get(currentFile)?.push({ start, end: start + count - 1 }); } } @@ -307,21 +331,20 @@ function parseGitDiff(diffOutput) { /** * Find all function/method/class nodes whose line ranges overlap any changed range. - * - * @param {import('better-sqlite3').Database} db - * @param {Map} changedRanges - * @param {boolean} noTests - * @returns {Array} */ -function findAffectedFunctions(db, changedRanges, noTests) { - const affectedFunctions = []; +function findAffectedFunctions( + db: any, + changedRanges: Map>, + noTests: boolean, +): any[] { + const affectedFunctions: any[] = []; for (const [file, ranges] of changedRanges) { if (noTests && isTestFile(file)) continue; const defs = db .prepare( `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`, ) - .all(file); + .all(file) as any[]; for (let i = 0; i < defs.length; i++) { const def = defs[i]; const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999); @@ -338,31 +361,25 @@ function findAffectedFunctions(db, changedRanges, noTests) { /** * Run BFS per affected function, collecting per-function results and the full affected set. - * - * @param {import('better-sqlite3').Database} db - * @param {Array} affectedFunctions - * @param {boolean} noTests - * @param {number} maxDepth - * @returns {{ functionResults: Array, allAffected: Set }} */ function buildFunctionImpactResults( - db, - affectedFunctions, - noTests, - maxDepth, + db: any, + affectedFunctions: any[], + noTests: boolean, + maxDepth: number, includeImplementors = true, -) { - const allAffected = new Set(); - const functionResults = affectedFunctions.map((fn) => { - const edges = []; - const idToKey = new Map(); +): { functionResults: any[]; allAffected: Set } { + const allAffected = new Set(); + const functionResults = affectedFunctions.map((fn: any) => { + const edges: any[] = []; + const idToKey = new Map(); idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`); const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, { noTests, maxDepth, includeImplementors, - onVisit(c, parentId) { + onVisit(c: any, parentId: number) { allAffected.add(`${c.file}:${c.name}`); const callerKey = `${c.file}::${c.name}:${c.line}`; idToKey.set(c.id, callerKey); @@ -387,14 +404,13 @@ function buildFunctionImpactResults( /** * Look up historically co-changed files for the set of changed files. * Returns an empty array if the co_changes table is unavailable. - * - * @param {import('better-sqlite3').Database} db - * @param {Map} changedRanges - * @param {Set} affectedFiles - * @param {boolean} noTests - * @returns {Array} */ -function lookupCoChanges(db, changedRanges, affectedFiles, noTests) { +function lookupCoChanges( + db: any, + changedRanges: Map, + affectedFiles: Set, + noTests: boolean, +): any[] { try { db.prepare('SELECT 1 FROM co_changes LIMIT 1').get(); const changedFilesList = [...changedRanges.keys()]; @@ -403,8 +419,8 @@ function lookupCoChanges(db, changedRanges, affectedFiles, noTests) { limit: 20, noTests, }); - return coResults.filter((r) => !affectedFiles.has(r.file)); - } catch (e) { + return coResults.filter((r: any) => !affectedFiles.has(r.file)); + } catch (e: any) { debug(`co_changes lookup skipped: ${e.message}`); return []; } @@ -413,13 +429,12 @@ function lookupCoChanges(db, changedRanges, affectedFiles, noTests) { /** * Look up CODEOWNERS for changed and affected files. * Returns null if no owners are found or lookup fails. - * - * @param {Map} changedRanges - * @param {Set} affectedFiles - * @param {string} repoRoot - * @returns {{ owners: object, affectedOwners: Array, suggestedReviewers: Array } | null} */ -function lookupOwnership(changedRanges, affectedFiles, repoRoot) { +function lookupOwnership( + changedRanges: Map, + affectedFiles: Set, + repoRoot: string, +): { owners: object; affectedOwners: string[]; suggestedReviewers: string[] } | null { try { const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])]; const ownerResult = ownersForFiles(allFilePaths, repoRoot); @@ -431,7 +446,7 @@ function lookupOwnership(changedRanges, affectedFiles, repoRoot) { }; } return null; - } catch (e) { + } catch (e: any) { debug(`CODEOWNERS lookup skipped: ${e.message}`); return null; } @@ -440,15 +455,14 @@ function lookupOwnership(changedRanges, affectedFiles, repoRoot) { /** * Check manifesto boundary violations scoped to the changed files. * Returns `{ boundaryViolations, boundaryViolationCount }`. - * - * @param {import('better-sqlite3').Database} db - * @param {Map} changedRanges - * @param {boolean} noTests - * @param {object} opts — full diffImpactData opts (may contain `opts.config`) - * @param {string} repoRoot - * @returns {{ boundaryViolations: Array, boundaryViolationCount: number }} */ -function checkBoundaryViolations(db, changedRanges, noTests, opts, repoRoot) { +function checkBoundaryViolations( + db: any, + changedRanges: Map, + noTests: boolean, + opts: any, + repoRoot: string, +): { boundaryViolations: any[]; boundaryViolationCount: number } { try { const cfg = opts.config || loadConfig(repoRoot); const boundaryConfig = cfg.manifesto?.boundaries; @@ -462,7 +476,7 @@ function checkBoundaryViolations(db, changedRanges, noTests, opts, repoRoot) { boundaryViolationCount: result.violationCount, }; } - } catch (e) { + } catch (e: any) { debug(`boundary check skipped: ${e.message}`); } return { boundaryViolations: [], boundaryViolationCount: 0 }; @@ -474,7 +488,19 @@ function checkBoundaryViolations(db, changedRanges, noTests, opts, repoRoot) { * Fix #2: Shell injection vulnerability. * Uses execFileSync instead of execSync to prevent shell interpretation of user input. */ -export function diffImpactData(customDbPath, opts = {}) { +export function diffImpactData( + customDbPath: string | undefined, + opts: { + noTests?: boolean; + config?: any; + depth?: number; + staged?: boolean; + ref?: string; + includeImplementors?: boolean; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -491,7 +517,7 @@ export function diffImpactData(customDbPath, opts = {}) { const gitResult = runGitDiff(repoRoot, opts); if (gitResult.error) return { error: gitResult.error }; - if (!gitResult.output.trim()) { + if (!gitResult.output?.trim()) { return { changedFiles: 0, newFiles: [], @@ -501,7 +527,7 @@ export function diffImpactData(customDbPath, opts = {}) { }; } - const { changedRanges, newFiles } = parseGitDiff(gitResult.output); + const { changedRanges, newFiles } = parseGitDiff(gitResult.output!); if (changedRanges.size === 0) { return { @@ -523,8 +549,8 @@ export function diffImpactData(customDbPath, opts = {}) { includeImplementors, ); - const affectedFiles = new Set(); - for (const key of allAffected) affectedFiles.add(key.split(':')[0]); + const affectedFiles = new Set(); + for (const key of allAffected) affectedFiles.add(key.split(':')[0]!); const historicallyCoupled = lookupCoChanges(db, changedRanges, affectedFiles, noTests); const ownership = lookupOwnership(changedRanges, affectedFiles, repoRoot); @@ -560,32 +586,44 @@ export function diffImpactData(customDbPath, opts = {}) { } } -export function diffImpactMermaid(customDbPath, opts = {}) { - const data = diffImpactData(customDbPath, opts); +export function diffImpactMermaid( + customDbPath: string | undefined, + opts: { + noTests?: boolean; + config?: any; + depth?: number; + staged?: boolean; + ref?: string; + includeImplementors?: boolean; + limit?: number; + offset?: number; + } = {}, +): string | object { + const data: any = diffImpactData(customDbPath, opts); if (data.error) return data.error; if (data.changedFiles === 0 || data.affectedFunctions.length === 0) { return 'flowchart TB\n none["No impacted functions detected"]'; } const newFileSet = new Set(data.newFiles || []); - const lines = ['flowchart TB']; + const lines: string[] = ['flowchart TB']; // Assign stable Mermaid node IDs let nodeCounter = 0; - const nodeIdMap = new Map(); - const nodeLabels = new Map(); - function nodeId(key, label) { + const nodeIdMap = new Map(); + const nodeLabels = new Map(); + function nodeId(key: string, label?: string): string { if (!nodeIdMap.has(key)) { nodeIdMap.set(key, `n${nodeCounter++}`); if (label) nodeLabels.set(key, label); } - return nodeIdMap.get(key); + return nodeIdMap.get(key)!; } // Register all nodes (changed functions + their callers) for (const fn of data.affectedFunctions) { nodeId(`${fn.file}::${fn.name}:${fn.line}`, fn.name); - for (const callers of Object.values(fn.levels || {})) { + for (const callers of Object.values(fn.levels || {}) as any[][]) { for (const c of callers) { nodeId(`${c.file}::${c.name}:${c.line}`, c.name); } @@ -593,10 +631,10 @@ export function diffImpactMermaid(customDbPath, opts = {}) { } // Collect all edges and determine blast radius - const allEdges = new Set(); - const edgeFromNodes = new Set(); - const edgeToNodes = new Set(); - const changedKeys = new Set(); + const allEdges = new Set(); + const edgeFromNodes = new Set(); + const edgeToNodes = new Set(); + const changedKeys = new Set(); for (const fn of data.affectedFunctions) { changedKeys.add(`${fn.file}::${fn.name}:${fn.line}`); @@ -611,7 +649,7 @@ export function diffImpactMermaid(customDbPath, opts = {}) { } // Blast radius: caller nodes that are never a source (leaf nodes of the impact tree) - const blastRadiusKeys = new Set(); + const blastRadiusKeys = new Set(); for (const key of edgeToNodes) { if (!edgeFromNodes.has(key) && !changedKeys.has(key)) { blastRadiusKeys.add(key); @@ -619,7 +657,7 @@ export function diffImpactMermaid(customDbPath, opts = {}) { } // Intermediate callers: not changed, not blast radius - const intermediateKeys = new Set(); + const intermediateKeys = new Set(); for (const key of edgeToNodes) { if (!changedKeys.has(key) && !blastRadiusKeys.has(key)) { intermediateKeys.add(key); @@ -627,10 +665,10 @@ export function diffImpactMermaid(customDbPath, opts = {}) { } // Group changed functions by file - const fileGroups = new Map(); + const fileGroups = new Map(); for (const fn of data.affectedFunctions) { if (!fileGroups.has(fn.file)) fileGroups.set(fn.file, []); - fileGroups.get(fn.file).push(fn); + fileGroups.get(fn.file)?.push(fn); } // Emit changed-file subgraphs @@ -667,7 +705,7 @@ export function diffImpactMermaid(customDbPath, opts = {}) { // Emit edges (impact flows from changed fn toward callers) for (const edgeKey of allEdges) { - const [from, to] = edgeKey.split('|'); + const [from, to] = edgeKey.split('|') as [string, string]; lines.push(` ${nodeIdMap.get(from)} --> ${nodeIdMap.get(to)}`); } diff --git a/src/domain/analysis/implementations.js b/src/domain/analysis/implementations.ts similarity index 80% rename from src/domain/analysis/implementations.js rename to src/domain/analysis/implementations.ts index 487f1948..3606a285 100644 --- a/src/domain/analysis/implementations.js +++ b/src/domain/analysis/implementations.ts @@ -13,7 +13,11 @@ import { findMatchingNodes } from './symbol-lookup.js'; * @param {{ noTests?: boolean, file?: string, kind?: string, limit?: number, offset?: number }} opts * @returns {{ name: string, results: Array<{ name: string, kind: string, file: string, line: number, implementors: Array<{ name: string, kind: string, file: string, line: number }> }> }} */ -export function implementationsData(name, customDbPath, opts = {}) { +export function implementationsData( + name: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; file?: string; kind?: string; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -29,7 +33,8 @@ export function implementationsData(name, customDbPath, opts = {}) { return { name, results: [] }; } - const results = nodes.map((node) => { + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + const results = (nodes as any[]).map((node) => { let implementors = findImplementors(db, node.id); if (noTests) implementors = implementors.filter((n) => !isTestFile(n.file)); @@ -57,9 +62,13 @@ export function implementationsData(name, customDbPath, opts = {}) { * @param {string} name - Class/struct name (partial match) * @param {string|undefined} customDbPath * @param {{ noTests?: boolean, file?: string, kind?: string, limit?: number, offset?: number }} opts - * @returns {{ name: string, results: Array<{ name: string, kind: string, file: string, line: number, interfaces: Array<{ name: string, kind: string, file: string, line: number }> }> }} + * @returns Object with name and results array containing interface info */ -export function interfacesData(name, customDbPath, opts = {}) { +export function interfacesData( + name: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; file?: string; kind?: string; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -75,7 +84,8 @@ export function interfacesData(name, customDbPath, opts = {}) { return { name, results: [] }; } - const results = nodes.map((node) => { + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + const results = (nodes as any[]).map((node) => { let interfaces = findInterfaces(db, node.id); if (noTests) interfaces = interfaces.filter((n) => !isTestFile(n.file)); diff --git a/src/domain/analysis/module-map.js b/src/domain/analysis/module-map.ts similarity index 68% rename from src/domain/analysis/module-map.js rename to src/domain/analysis/module-map.ts index d3566c8c..e2d3c79b 100644 --- a/src/domain/analysis/module-map.js +++ b/src/domain/analysis/module-map.ts @@ -43,35 +43,44 @@ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20; // Section helpers // --------------------------------------------------------------------------- -function buildTestFileIds(db) { - const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all(); - const testFileIds = new Set(); - const testFiles = new Set(); +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function buildTestFileIds(db: any): Set { + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all() as any[]; + const testFileIds = new Set(); + const testFiles = new Set(); for (const n of allFileNodes) { if (isTestFile(n.file)) { testFileIds.add(n.id); testFiles.add(n.file); } } - const allNodes = db.prepare('SELECT id, file FROM nodes').all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const allNodes = db.prepare('SELECT id, file FROM nodes').all() as any[]; for (const n of allNodes) { if (testFiles.has(n.file)) testFileIds.add(n.id); } return testFileIds; } -function countNodesByKind(db, testFileIds) { - let nodeRows; +function countNodesByKind( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + testFileIds: Set | null, +): { total: number; byKind: Record } { + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + let nodeRows: any[]; if (testFileIds) { - const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all() as any[]; const filtered = allNodes.filter((n) => !testFileIds.has(n.id)); - const counts = {}; + const counts: Record = {}; for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1; nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c })); } else { nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all(); } - const byKind = {}; + const byKind: Record = {}; let total = 0; for (const r of nodeRows) { byKind[r.kind] = r.c; @@ -80,20 +89,26 @@ function countNodesByKind(db, testFileIds) { return { total, byKind }; } -function countEdgesByKind(db, testFileIds) { - let edgeRows; +function countEdgesByKind( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + testFileIds: Set | null, +): { total: number; byKind: Record } { + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + let edgeRows: any[]; if (testFileIds) { - const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all() as any[]; const filtered = allEdges.filter( (e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id), ); - const counts = {}; + const counts: Record = {}; for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1; edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c })); } else { edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all(); } - const byKind = {}; + const byKind: Record = {}; let total = 0; for (const r of edgeRows) { byKind[r.kind] = r.c; @@ -102,16 +117,21 @@ function countEdgesByKind(db, testFileIds) { return { total, byKind }; } -function countFilesByLanguage(db, noTests) { - const extToLang = new Map(); +function countFilesByLanguage( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + noTests: boolean, +): { total: number; languages: number; byLanguage: Record } { + const extToLang = new Map(); for (const entry of LANGUAGE_REGISTRY) { for (const ext of entry.extensions) { extToLang.set(ext, entry.id); } } - let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all() as any[]; if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file)); - const byLanguage = {}; + const byLanguage: Record = {}; for (const row of fileNodes) { const ext = path.extname(row.file).toLowerCase(); const lang = extToLang.get(ext) || 'other'; @@ -120,7 +140,12 @@ function countFilesByLanguage(db, noTests) { return { total: fileNodes.length, languages: Object.keys(byLanguage).length, byLanguage }; } -function findHotspots(db, noTests, limit) { +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function findHotspots( + db: any, + noTests: boolean, + limit: number, +): { file: string; fanIn: number; fanOut: number }[] { const testFilter = testFilterSQL('n.file', noTests); const hotspotRows = db .prepare(` @@ -132,7 +157,8 @@ function findHotspots(db, noTests, limit) { ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id) + (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC `) - .all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + .all() as any[]; const filtered = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows; return filtered.slice(0, limit).map((r) => ({ file: r.file, @@ -141,27 +167,35 @@ function findHotspots(db, noTests, limit) { })); } -function getEmbeddingsInfo(db) { +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function getEmbeddingsInfo(db: any): object | null { try { - const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get() as any; if (count && count.c > 0) { - const meta = {}; - const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all(); + const meta: Record = {}; + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all() as any[]; for (const r of metaRows) meta[r.key] = r.value; return { count: count.c, - model: meta.model || null, - dim: meta.dim ? parseInt(meta.dim, 10) : null, - builtAt: meta.built_at || null, + model: meta['model'] || null, + dim: meta['dim'] ? parseInt(meta['dim'], 10) : null, + builtAt: meta['built_at'] || null, }; } } catch (e) { - debug(`embeddings lookup skipped: ${e.message}`); + debug(`embeddings lookup skipped: ${(e as Error).message}`); } return null; } -function computeQualityMetrics(db, testFilter, fpThreshold = FALSE_POSITIVE_CALLER_THRESHOLD) { +function computeQualityMetrics( + // biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 + db: any, + testFilter: string, + fpThreshold = FALSE_POSITIVE_CALLER_THRESHOLD, +): object { const qualityTestFilter = testFilter.replace(/n\.file/g, 'file'); const totalCallable = db @@ -194,7 +228,8 @@ function computeQualityMetrics(db, testFilter, fpThreshold = FALSE_POSITIVE_CALL HAVING caller_count > ? ORDER BY caller_count DESC `) - .all(fpThreshold); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + .all(fpThreshold) as any[]; const falsePositiveWarnings = fpRows .filter((r) => FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name), @@ -225,12 +260,17 @@ function computeQualityMetrics(db, testFilter, fpThreshold = FALSE_POSITIVE_CALL }; } -function countRoles(db, noTests) { - let roleRows; +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function countRoles(db: any, noTests: boolean): Record { + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + let roleRows: any[]; if (noTests) { - const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all(); + const allRoleNodes = db + .prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL') + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + .all() as any[]; const filtered = allRoleNodes.filter((n) => !isTestFile(n.file)); - const counts = {}; + const counts: Record = {}; for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1; roleRows = Object.entries(counts).map(([role, c]) => ({ role, c })); } else { @@ -238,17 +278,18 @@ function countRoles(db, noTests) { .prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role') .all(); } - const roles = {}; + const roles: Record = {}; let deadTotal = 0; for (const r of roleRows) { roles[r.role] = r.c; if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.c; } - if (deadTotal > 0) roles.dead = deadTotal; + if (deadTotal > 0) roles['dead'] = deadTotal; return roles; } -function getComplexitySummary(db, testFilter) { +// biome-ignore lint/suspicious/noExplicitAny: db handle from better-sqlite3 +function getComplexitySummary(db: any, testFilter: string): object | null { try { const cRows = db .prepare( @@ -256,21 +297,26 @@ function getComplexitySummary(db, testFilter) { FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id WHERE n.kind IN ('function','method') ${testFilter}`, ) - .all(); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + .all() as any[]; if (cRows.length > 0) { const miValues = cRows.map((r) => r.maintainability_index || 0); return { analyzed: cRows.length, - avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1), - avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1), + avgCognitive: +(cRows.reduce((s: number, r) => s + r.cognitive, 0) / cRows.length).toFixed( + 1, + ), + avgCyclomatic: +( + cRows.reduce((s: number, r) => s + r.cyclomatic, 0) / cRows.length + ).toFixed(1), maxCognitive: Math.max(...cRows.map((r) => r.cognitive)), maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)), - avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1), + avgMI: +(miValues.reduce((s: number, v: number) => s + v, 0) / miValues.length).toFixed(1), minMI: +Math.min(...miValues).toFixed(1), }; } } catch (e) { - debug(`complexity summary skipped: ${e.message}`); + debug(`complexity summary skipped: ${(e as Error).message}`); } return null; } @@ -279,7 +325,11 @@ function getComplexitySummary(db, testFilter) { // Public API // --------------------------------------------------------------------------- -export function moduleMapData(customDbPath, limit = 20, opts = {}) { +export function moduleMapData( + customDbPath: string | undefined, + limit = 20, + opts: { noTests?: boolean } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -297,7 +347,8 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) { ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC LIMIT ? `) - .all(limit); + // biome-ignore lint/suspicious/noExplicitAny: untyped SQLite row + .all(limit) as any[]; const topNodes = nodes.map((n) => ({ file: n.file, @@ -317,7 +368,11 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) { } } -export function statsData(customDbPath, opts = {}) { +export function statsData( + customDbPath: string | undefined, + // biome-ignore lint/suspicious/noExplicitAny: config shape varies by caller + opts: { noTests?: boolean; config?: any } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; diff --git a/src/domain/analysis/roles.js b/src/domain/analysis/roles.ts similarity index 74% rename from src/domain/analysis/roles.js rename to src/domain/analysis/roles.ts index 403f758c..a496818e 100644 --- a/src/domain/analysis/roles.js +++ b/src/domain/analysis/roles.ts @@ -5,13 +5,22 @@ import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js'; import { normalizeSymbol } from '../../shared/normalize.js'; import { paginateResult } from '../../shared/paginate.js'; -export function rolesData(customDbPath, opts = {}) { +export function rolesData( + customDbPath: string | undefined, + opts: { + noTests?: boolean; + role?: string | null; + file?: string; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; const filterRole = opts.role || null; - const conditions = ['role IS NOT NULL']; - const params = []; + const conditions: string[] = ['role IS NOT NULL']; + const params: unknown[] = []; if (filterRole) { if (filterRole === DEAD_ROLE_PREFIX) { @@ -23,7 +32,7 @@ export function rolesData(customDbPath, opts = {}) { } } { - const fc = buildFileConditionSQL(opts.file, 'file'); + const fc = buildFileConditionSQL(opts.file ?? '', 'file'); if (fc.sql) { // Strip leading ' AND ' since we're using conditions array conditions.push(fc.sql.replace(/^ AND /, '')); @@ -35,11 +44,12 @@ export function rolesData(customDbPath, opts = {}) { .prepare( `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`, ) - .all(...params); + // biome-ignore lint/suspicious/noExplicitAny: DB row types not yet migrated + .all(...params) as any[]; if (noTests) rows = rows.filter((r) => !isTestFile(r.file)); - const summary = {}; + const summary: Record = {}; for (const r of rows) { summary[r.role] = (summary[r.role] || 0) + 1; } diff --git a/src/domain/analysis/symbol-lookup.js b/src/domain/analysis/symbol-lookup.ts similarity index 65% rename from src/domain/analysis/symbol-lookup.js rename to src/domain/analysis/symbol-lookup.ts index ffb5566c..a5252ab6 100644 --- a/src/domain/analysis/symbol-lookup.js +++ b/src/domain/analysis/symbol-lookup.ts @@ -19,8 +19,9 @@ import { isTestFile } from '../../infrastructure/test-filter.js'; import { EVERY_SYMBOL_KIND } from '../../shared/kinds.js'; import { getFileHash, normalizeSymbol } from '../../shared/normalize.js'; import { paginateResult } from '../../shared/paginate.js'; +import type { SymbolKind } from '../../types.js'; -const FUNCTION_KINDS = ['function', 'method', 'class', 'constant']; +const FUNCTION_KINDS: SymbolKind[] = ['function', 'method', 'class', 'constant']; /** * Find nodes matching a name query, ranked by relevance. @@ -28,22 +29,30 @@ const FUNCTION_KINDS = ['function', 'method', 'class', 'constant']; * * @param {object} dbOrRepo - A better-sqlite3 Database or a Repository instance */ -export function findMatchingNodes(dbOrRepo, name, opts = {}) { - const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS; +export function findMatchingNodes( + dbOrRepo: any, + name: string, + opts: { kind?: string; kinds?: string[]; noTests?: boolean; file?: string } = {}, +): any[] { + const kinds: SymbolKind[] = opts.kind + ? [opts.kind as SymbolKind] + : opts.kinds?.length + ? (opts.kinds as SymbolKind[]) + : FUNCTION_KINDS; const isRepo = dbOrRepo instanceof Repository; const rows = isRepo ? dbOrRepo.findNodesWithFanIn(`%${name}%`, { kinds, file: opts.file }) : findNodesWithFanIn(dbOrRepo, `%${name}%`, { kinds, file: opts.file }); - const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows; + const nodes: any[] = opts.noTests ? rows.filter((n: any) => !isTestFile(n.file)) : rows; const lowerQuery = name.toLowerCase(); for (const node of nodes) { const lowerName = node.name.toLowerCase(); const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName; - let matchScore; + let matchScore: number; if (lowerName === lowerQuery || bareName === lowerQuery) { matchScore = 100; } else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) { @@ -58,41 +67,45 @@ export function findMatchingNodes(dbOrRepo, name, opts = {}) { node._relevance = matchScore + fanInBonus; } - nodes.sort((a, b) => b._relevance - a._relevance); + nodes.sort((a: any, b: any) => b._relevance - a._relevance); return nodes; } -export function queryNameData(name, customDbPath, opts = {}) { +export function queryNameData( + name: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; - let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`); - if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file)); + let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`) as any[]; + if (noTests) nodes = nodes.filter((n: any) => !isTestFile(n.file)); if (nodes.length === 0) { return { query: name, results: [] }; } const hc = new Map(); - const results = nodes.map((node) => { + const results = nodes.map((node: any) => { let callees = findAllOutgoingEdges(db, node.id); let callers = findAllIncomingEdges(db, node.id); if (noTests) { - callees = callees.filter((c) => !isTestFile(c.file)); - callers = callers.filter((c) => !isTestFile(c.file)); + callees = callees.filter((c: any) => !isTestFile(c.file)); + callers = callers.filter((c: any) => !isTestFile(c.file)); } return { ...normalizeSymbol(node, db, hc), - callees: callees.map((c) => ({ + callees: callees.map((c: any) => ({ name: c.name, kind: c.kind, file: c.file, line: c.line, edgeKind: c.edge_kind, })), - callers: callers.map((c) => ({ + callers: callers.map((c: any) => ({ name: c.name, kind: c.kind, file: c.file, @@ -109,50 +122,50 @@ export function queryNameData(name, customDbPath, opts = {}) { } } -function whereSymbolImpl(db, target, noTests) { +function whereSymbolImpl(db: any, target: string, noTests: boolean): any[] { const placeholders = EVERY_SYMBOL_KIND.map(() => '?').join(', '); let nodes = db .prepare( `SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`, ) - .all(`%${target}%`, ...EVERY_SYMBOL_KIND); - if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file)); + .all(`%${target}%`, ...EVERY_SYMBOL_KIND) as any[]; + if (noTests) nodes = nodes.filter((n: any) => !isTestFile(n.file)); const hc = new Map(); - return nodes.map((node) => { + return nodes.map((node: any) => { const crossCount = countCrossFileCallers(db, node.id, node.file); const exported = crossCount > 0; let uses = findCallers(db, node.id); - if (noTests) uses = uses.filter((u) => !isTestFile(u.file)); + if (noTests) uses = uses.filter((u: any) => !isTestFile(u.file)); return { ...normalizeSymbol(node, db, hc), exported, - uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })), + uses: uses.map((u: any) => ({ name: u.name, file: u.file, line: u.line })), }; }); } -function whereFileImpl(db, target) { +function whereFileImpl(db: any, target: string): any[] { const fileNodes = findFileNodes(db, `%${target}%`); if (fileNodes.length === 0) return []; - return fileNodes.map((fn) => { + return fileNodes.map((fn: any) => { const symbols = findNodesByFile(db, fn.file); - const imports = findImportTargets(db, fn.id).map((r) => r.file); + const imports = findImportTargets(db, fn.id).map((r: any) => r.file); - const importedBy = findImportSources(db, fn.id).map((r) => r.file); + const importedBy = findImportSources(db, fn.id).map((r: any) => r.file); const exportedIds = findCrossFileCallTargets(db, fn.file); - const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name); + const exported = symbols.filter((s: any) => exportedIds.has(s.id)).map((s: any) => s.name); return { file: fn.file, fileHash: getFileHash(db, fn.file), - symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })), + symbols: symbols.map((s: any) => ({ name: s.name, kind: s.kind, line: s.line })), imports, importedBy, exported, @@ -160,7 +173,11 @@ function whereFileImpl(db, target) { }); } -export function whereData(target, customDbPath, opts = {}) { +export function whereData( + target: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; file?: boolean; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -175,17 +192,26 @@ export function whereData(target, customDbPath, opts = {}) { } } -export function listFunctionsData(customDbPath, opts = {}) { +export function listFunctionsData( + customDbPath: string | undefined, + opts: { + noTests?: boolean; + file?: string; + pattern?: string; + limit?: number; + offset?: number; + } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; let rows = listFunctionNodes(db, { file: opts.file, pattern: opts.pattern }); - if (noTests) rows = rows.filter((r) => !isTestFile(r.file)); + if (noTests) rows = rows.filter((r: any) => !isTestFile(r.file)); const hc = new Map(); - const functions = rows.map((r) => normalizeSymbol(r, db, hc)); + const functions = rows.map((r: any) => normalizeSymbol(r, db, hc)); const base = { count: functions.length, functions }; return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset }); } finally { @@ -193,7 +219,11 @@ export function listFunctionsData(customDbPath, opts = {}) { } } -export function childrenData(name, customDbPath, opts = {}) { +export function childrenData( + name: string, + customDbPath: string | undefined, + opts: { noTests?: boolean; file?: string; kind?: string; limit?: number; offset?: number } = {}, +): object { const db = openReadonlyOrFail(customDbPath); try { const noTests = opts.noTests || false; @@ -203,15 +233,15 @@ export function childrenData(name, customDbPath, opts = {}) { return { name, results: [] }; } - const results = nodes.map((node) => { - let children; + const results = nodes.map((node: any) => { + let children: any[]; try { children = findNodeChildren(db, node.id); - } catch (e) { + } catch (e: any) { debug(`findNodeChildren failed for node ${node.id}: ${e.message}`); children = []; } - if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file)); + if (noTests) children = children.filter((c: any) => !isTestFile(c.file || node.file)); return { name: node.name, kind: node.kind, @@ -220,7 +250,7 @@ export function childrenData(name, customDbPath, opts = {}) { scope: node.scope || null, visibility: node.visibility || null, qualifiedName: node.qualified_name || null, - children: children.map((c) => ({ + children: children.map((c: any) => ({ name: c.name, kind: c.kind, line: c.line, diff --git a/src/domain/graph/resolve.js b/src/domain/graph/resolve.ts similarity index 83% rename from src/domain/graph/resolve.js rename to src/domain/graph/resolve.ts index 0eb5db8d..cf2c5d79 100644 --- a/src/domain/graph/resolve.js +++ b/src/domain/graph/resolve.ts @@ -2,11 +2,13 @@ import fs from 'node:fs'; import path from 'node:path'; import { loadNative } from '../../infrastructure/native.js'; import { normalizePath } from '../../shared/constants.js'; +import type { BareSpecifier, BatchResolvedMap, ImportBatchItem, PathAliases } from '../../types.js'; // ── package.json exports resolution ───────────────────────────────── /** Cache: packageDir → parsed exports field (or null) */ -const _exportsCache = new Map(); +// biome-ignore lint/suspicious/noExplicitAny: package.json exports field has no fixed schema +const _exportsCache: Map = new Map(); /** * Parse a bare specifier into { packageName, subpath }. @@ -14,8 +16,8 @@ const _exportsCache = new Map(); * Plain: "pkg/sub" → { packageName: "pkg", subpath: "./sub" } * No sub: "pkg" → { packageName: "pkg", subpath: "." } */ -export function parseBareSpecifier(specifier) { - let packageName, rest; +export function parseBareSpecifier(specifier: string): BareSpecifier | null { + let packageName: string, rest: string; if (specifier.startsWith('@')) { const parts = specifier.split('/'); if (parts.length < 2) return null; @@ -38,7 +40,7 @@ export function parseBareSpecifier(specifier) { * Find the package directory for a given package name, starting from rootDir. * Walks up node_modules directories. */ -function findPackageDir(packageName, rootDir) { +function findPackageDir(packageName: string, rootDir: string): string | null { let dir = rootDir; while (true) { const candidate = path.join(dir, 'node_modules', packageName); @@ -53,7 +55,8 @@ function findPackageDir(packageName, rootDir) { * Read and cache the exports field from a package's package.json. * Returns the exports value or null. */ -function getPackageExports(packageDir) { +// biome-ignore lint/suspicious/noExplicitAny: package.json exports field has no fixed schema +function getPackageExports(packageDir: string): any { if (_exportsCache.has(packageDir)) return _exportsCache.get(packageDir); try { const raw = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'); @@ -68,13 +71,13 @@ function getPackageExports(packageDir) { } /** Condition names to try, in priority order. */ -const CONDITION_ORDER = ['import', 'require', 'default']; +const CONDITION_ORDER: readonly string[] = ['import', 'require', 'default']; /** * Resolve a conditional exports value (string, object with conditions, or array). * Returns a string target or null. */ -function resolveCondition(value) { +function resolveCondition(value: unknown): string | null { if (typeof value === 'string') return value; if (Array.isArray(value)) { for (const item of value) { @@ -85,7 +88,8 @@ function resolveCondition(value) { } if (value && typeof value === 'object') { for (const cond of CONDITION_ORDER) { - if (cond in value) return resolveCondition(value[cond]); + if (cond in (value as Record)) + return resolveCondition((value as Record)[cond]); } return null; } @@ -96,7 +100,7 @@ function resolveCondition(value) { * Match a subpath against an exports map key that uses a wildcard pattern. * Key: "./lib/*" matches subpath "./lib/foo/bar" → substitution "foo/bar" */ -function matchSubpathPattern(pattern, subpath) { +function matchSubpathPattern(pattern: string, subpath: string): string | null { const starIdx = pattern.indexOf('*'); if (starIdx === -1) return null; const prefix = pattern.slice(0, starIdx); @@ -112,7 +116,7 @@ function matchSubpathPattern(pattern, subpath) { * Resolve a bare specifier through the package.json exports field. * Returns an absolute path or null. */ -export function resolveViaExports(specifier, rootDir) { +export function resolveViaExports(specifier: string, rootDir: string): string | null { const parsed = parseBareSpecifier(specifier); if (!parsed) return null; @@ -189,25 +193,26 @@ export function resolveViaExports(specifier, rootDir) { } /** Clear the exports cache (for testing). */ -export function clearExportsCache() { +export function clearExportsCache(): void { _exportsCache.clear(); } // ── Monorepo workspace resolution ─────────────────────────────────── /** Cache: rootDir → Map */ -const _workspaceCache = new Map(); +const _workspaceCache: Map> = new Map(); /** Set of resolved relative paths that came from workspace resolution. */ -const _workspaceResolvedPaths = new Set(); +const _workspaceResolvedPaths: Set = new Set(); /** * Set the workspace map for a given rootDir. * Called by the build pipeline after detecting workspaces. - * @param {string} rootDir - * @param {Map} map */ -export function setWorkspaces(rootDir, map) { +export function setWorkspaces( + rootDir: string, + map: Map, +): void { _workspaceCache.set(rootDir, map); _workspaceResolvedPaths.clear(); _exportsCache.clear(); @@ -216,7 +221,7 @@ export function setWorkspaces(rootDir, map) { /** * Get workspace packages for a rootDir. Returns empty map if not set. */ -function getWorkspaces(rootDir) { +function getWorkspaces(rootDir: string): Map { return _workspaceCache.get(rootDir) || new Map(); } @@ -226,9 +231,9 @@ function getWorkspaces(rootDir) { * For "@myorg/utils" → finds the workspace package dir → resolves entry point. * For "@myorg/utils/sub" → finds package dir → tries exports field → filesystem probe. * - * @returns {string|null} Absolute path to resolved file, or null. + * @returns Absolute path to resolved file, or null. */ -export function resolveViaWorkspace(specifier, rootDir) { +export function resolveViaWorkspace(specifier: string, rootDir: string): string | null { const parsed = parseBareSpecifier(specifier); if (!parsed) return null; @@ -293,12 +298,12 @@ export function resolveViaWorkspace(specifier, rootDir) { * Check if a resolved relative path was resolved via workspace detection. * Used by computeConfidence to assign high confidence (0.95) to workspace imports. */ -export function isWorkspaceResolved(resolvedPath) { +export function isWorkspaceResolved(resolvedPath: string): boolean { return _workspaceResolvedPaths.has(resolvedPath); } /** Clear workspace caches (for testing). */ -export function clearWorkspaceCache() { +export function clearWorkspaceCache(): void { _workspaceCache.clear(); _workspaceResolvedPaths.clear(); } @@ -309,7 +314,9 @@ export function clearWorkspaceCache() { * Convert JS alias format { baseUrl, paths: { pattern: [targets] } } * to native format { baseUrl, paths: [{ pattern, targets }] }. */ -export function convertAliasesForNative(aliases) { +export function convertAliasesForNative( + aliases: PathAliases | null | undefined, +): { baseUrl: string; paths: { pattern: string; targets: string[] }[] } | null { if (!aliases) return null; return { baseUrl: aliases.baseUrl || '', @@ -322,7 +329,11 @@ export function convertAliasesForNative(aliases) { // ── JS fallback implementations ───────────────────────────────────── -function resolveViaAlias(importSource, aliases, _rootDir) { +function resolveViaAlias( + importSource: string, + aliases: PathAliases, + _rootDir: string, +): string | null { if (aliases.baseUrl && !importSource.startsWith('.') && !importSource.startsWith('/')) { const candidate = path.resolve(aliases.baseUrl, importSource); for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']) { @@ -355,7 +366,12 @@ function resolveViaAlias(importSource, aliases, _rootDir) { return null; } -function resolveImportPathJS(fromFile, importSource, rootDir, aliases) { +function resolveImportPathJS( + fromFile: string, + importSource: string, + rootDir: string, + aliases: PathAliases | null, +): string { if (!importSource.startsWith('.') && aliases) { const aliasResolved = resolveViaAlias(importSource, aliases, rootDir); if (aliasResolved) return normalizePath(path.relative(rootDir, aliasResolved)); @@ -404,7 +420,11 @@ function resolveImportPathJS(fromFile, importSource, rootDir, aliases) { return normalizePath(path.relative(rootDir, resolved)); } -function computeConfidenceJS(callerFile, targetFile, importedFrom) { +function computeConfidenceJS( + callerFile: string, + targetFile: string, + importedFrom: string | null, +): number { if (!targetFile || !callerFile) return 0.3; if (callerFile === targetFile) return 1.0; if (importedFrom === targetFile) return 1.0; @@ -423,7 +443,12 @@ function computeConfidenceJS(callerFile, targetFile, importedFrom) { * Resolve a single import path. * Tries native, falls back to JS. */ -export function resolveImportPath(fromFile, importSource, rootDir, aliases) { +export function resolveImportPath( + fromFile: string, + importSource: string, + rootDir: string, + aliases: PathAliases | null, +): string { const native = loadNative(); if (native) { try { @@ -445,7 +470,11 @@ export function resolveImportPath(fromFile, importSource, rootDir, aliases) { * Compute proximity-based confidence for call resolution. * Tries native, falls back to JS. */ -export function computeConfidence(callerFile, targetFile, importedFrom) { +export function computeConfidence( + callerFile: string, + targetFile: string, + importedFrom: string | null, +): number { const native = loadNative(); if (native) { try { @@ -460,12 +489,13 @@ export function computeConfidence(callerFile, targetFile, importedFrom) { /** * Batch resolve multiple imports in a single native call. * Returns Map<"fromFile|importSource", resolvedPath> or null when native unavailable. - * @param {Array} inputs - Array of { fromFile, importSource } - * @param {string} rootDir - Project root - * @param {object} aliases - Path aliases - * @param {string[]} [knownFiles] - Optional file paths for FS cache (avoids syscalls) */ -export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) { +export function resolveImportsBatch( + inputs: ImportBatchItem[], + rootDir: string, + aliases: PathAliases | null, + knownFiles?: string[] | null, +): BatchResolvedMap | null { const native = loadNative(); if (!native) return null; @@ -480,7 +510,7 @@ export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) { convertAliasesForNative(aliases), knownFiles || null, ); - const map = new Map(); + const map: BatchResolvedMap = new Map(); for (const r of results) { map.set(`${r.fromFile}|${r.importSource}`, normalizePath(path.normalize(r.resolvedPath))); } diff --git a/src/domain/parser.js b/src/domain/parser.ts similarity index 82% rename from src/domain/parser.js rename to src/domain/parser.ts index 59a4a10c..ea83066e 100644 --- a/src/domain/parser.js +++ b/src/domain/parser.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'; 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, LanguageRegistryEntry } from '../types.js'; // Re-export all extractors for backward compatibility export { @@ -32,23 +33,23 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); -function grammarPath(name) { +function grammarPath(name: string): string { return path.join(__dirname, '..', '..', 'grammars', name); } let _initialized = false; // Memoized parsers — avoids reloading WASM grammars on every createParsers() call -let _cachedParsers = null; +let _cachedParsers: Map | null = null; // Cached Language objects — WASM-backed, must be .delete()'d explicitly -let _cachedLanguages = null; +let _cachedLanguages: Map | null = null; // Query cache for JS/TS/TSX extractors (populated during createParsers) -const _queryCache = new Map(); +const _queryCache: Map = new Map(); // Shared patterns for all JS/TS/TSX (class_declaration excluded — name type differs) -const COMMON_QUERY_PATTERNS = [ +const COMMON_QUERY_PATTERNS: string[] = [ '(function_declaration name: (identifier) @fn_name) @fn_node', '(variable_declarator name: (identifier) @varfn_name value: (arrow_function) @varfn_value)', '(variable_declarator name: (identifier) @varfn_name value: (function_expression) @varfn_value)', @@ -65,13 +66,13 @@ const COMMON_QUERY_PATTERNS = [ const JS_CLASS_PATTERN = '(class_declaration name: (identifier) @cls_name) @cls_node'; // TS/TSX: class name is (type_identifier), plus interface and type alias -const TS_EXTRA_PATTERNS = [ +const TS_EXTRA_PATTERNS: string[] = [ '(class_declaration name: (type_identifier) @cls_name) @cls_node', '(interface_declaration name: (type_identifier) @iface_name) @iface_node', '(type_alias_declaration name: (type_identifier) @type_name) @type_node', ]; -export async function createParsers() { +export async function createParsers(): Promise> { if (_cachedParsers) return _cachedParsers; if (!_initialized) { @@ -79,8 +80,8 @@ export async function createParsers() { _initialized = true; } - const parsers = new Map(); - const languages = new Map(); + const parsers: Map = new Map(); + const languages: Map = new Map(); for (const entry of LANGUAGE_REGISTRY) { try { const lang = await Language.load(grammarPath(entry.grammarFile)); @@ -96,7 +97,7 @@ export async function createParsers() { : [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN]; _queryCache.set(entry.id, new Query(lang, patterns.join('\n'))); } - } catch (e) { + } catch (e: any) { if (entry.required) throw e; warn( `${entry.id} parser failed to initialize: ${e.message}. ${entry.id} files will be skipped.`, @@ -114,13 +115,13 @@ export async function createParsers() { * Call this between repeated builds in the same process (e.g. benchmarks) * to prevent memory accumulation that can cause segfaults. */ -export function disposeParsers() { +export function disposeParsers(_parsers?: Map): void { if (_cachedParsers) { for (const [id, parser] of _cachedParsers) { if (parser && typeof parser.delete === 'function') { try { parser.delete(); - } catch (e) { + } catch (e: any) { debug(`Failed to dispose parser ${id}: ${e.message}`); } } @@ -131,7 +132,7 @@ export function disposeParsers() { if (query && typeof query.delete === 'function') { try { query.delete(); - } catch (e) { + } catch (e: any) { debug(`Failed to dispose query ${id}: ${e.message}`); } } @@ -142,7 +143,7 @@ export function disposeParsers() { if (lang && typeof lang.delete === 'function') { try { lang.delete(); - } catch (e) { + } catch (e: any) { debug(`Failed to dispose language ${id}: ${e.message}`); } } @@ -152,7 +153,7 @@ export function disposeParsers() { _initialized = false; } -export function getParser(parsers, filePath) { +export function getParser(parsers: Map, filePath: string): any | null { const ext = path.extname(filePath); const entry = _extToLang.get(ext); if (!entry) return null; @@ -163,11 +164,11 @@ export function getParser(parsers, filePath) { * Pre-parse files missing `_tree` via WASM so downstream phases (CFG, dataflow) * don't each need to create parsers and re-parse independently. * Only parses files whose extension is in SUPPORTED_EXTENSIONS. - * - * @param {Map} fileSymbols - Map - * @param {string} rootDir - absolute project root */ -export async function ensureWasmTrees(fileSymbols, rootDir) { +export async function ensureWasmTrees( + fileSymbols: Map, + rootDir: string, +): Promise { // Check if any file needs a tree let needsParse = false; for (const [relPath, symbols] of fileSymbols) { @@ -192,17 +193,17 @@ export async function ensureWasmTrees(fileSymbols, rootDir) { if (!parser) continue; const absPath = path.join(rootDir, relPath); - let code; + let code: string; try { code = fs.readFileSync(absPath, 'utf-8'); - } catch (e) { + } catch (e: any) { debug(`ensureWasmTrees: cannot read ${relPath}: ${e.message}`); continue; } try { symbols._tree = parser.parse(code); symbols._langId = entry.id; - } catch (e) { + } catch (e: any) { debug(`ensureWasmTrees: parse failed for ${relPath}: ${e.message}`); } } @@ -211,7 +212,7 @@ export async function ensureWasmTrees(fileSymbols, rootDir) { /** * Check whether the required WASM grammar files exist on disk. */ -export function isWasmAvailable() { +export function isWasmAvailable(): boolean { return LANGUAGE_REGISTRY.filter((e) => e.required).every((e) => fs.existsSync(grammarPath(e.grammarFile)), ); @@ -219,7 +220,12 @@ export function isWasmAvailable() { // ── Unified API ────────────────────────────────────────────────────────────── -function resolveEngine(opts = {}) { +interface ResolvedEngine { + name: string; + native: any; +} + +function resolveEngine(opts: { engine?: EngineMode; nativeEngine?: any } = {}): ResolvedEngine { const pref = opts.engine || 'auto'; if (pref === 'wasm') return { name: 'wasm', native: null }; if (pref === 'native' || pref === 'auto') { @@ -240,7 +246,7 @@ function resolveEngine(opts = {}) { * - Backward compat for older native binaries missing js_name annotations * - dataflow argFlows/mutations bindingType → binding wrapper */ -function patchNativeResult(r) { +function patchNativeResult(r: any): any { // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count" r.lineCount = r.lineCount ?? r.line_count ?? null; r._lineCount = r.lineCount; @@ -289,7 +295,7 @@ function patchNativeResult(r) { * Declarative registry of all supported languages. * Adding a new language requires only a new entry here + its extractor function. */ -export const LANGUAGE_REGISTRY = [ +export const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [ { id: 'javascript', extensions: ['.js', '.jsx', '.mjs', '.cjs'], @@ -369,14 +375,14 @@ export const LANGUAGE_REGISTRY = [ }, ]; -const _extToLang = new Map(); +const _extToLang: Map = new Map(); for (const entry of LANGUAGE_REGISTRY) { for (const ext of entry.extensions) { _extToLang.set(ext, entry); } } -export const SUPPORTED_EXTENSIONS = new Set(_extToLang.keys()); +export const SUPPORTED_EXTENSIONS: Set = new Set(_extToLang.keys()); /** * WASM-based typeMap backfill for older native binaries that don't emit typeMap. @@ -384,7 +390,10 @@ export const SUPPORTED_EXTENSIONS = new Set(_extToLang.keys()); * matches inside comments and string literals. * TODO: Remove once all published native binaries include typeMap extraction (>= 3.2.0) */ -async function backfillTypeMap(filePath, source) { +async function backfillTypeMap( + filePath: string, + source: string, +): Promise<{ typeMap: any; backfilled: boolean }> { let code = source; if (!code) { try { @@ -399,9 +408,9 @@ async function backfillTypeMap(filePath, source) { if (!extracted?.symbols?.typeMap) { return { typeMap: [], backfilled: false }; } - const tm = extracted.symbols.typeMap; + const tm: any = extracted.symbols.typeMap; return { - typeMap: tm instanceof Map ? tm : new Map(tm.map((e) => [e.name, e.typeName])), + typeMap: tm instanceof Map ? tm : new Map(tm.map((e: any) => [e.name, e.typeName])), backfilled: true, }; } finally { @@ -417,14 +426,18 @@ async function backfillTypeMap(filePath, source) { /** * WASM extraction helper: picks the right extractor based on file extension. */ -function wasmExtractSymbols(parsers, filePath, code) { +function wasmExtractSymbols( + parsers: Map, + filePath: string, + code: string, +): { symbols: ExtractorOutput; tree: any; langId: string } | null { const parser = getParser(parsers, filePath); if (!parser) return null; - let tree; + let tree: any; try { tree = parser.parse(code); - } catch (e) { + } catch (e: any) { warn(`Parse error in ${filePath}: ${e.message}`); return null; } @@ -439,13 +452,20 @@ function wasmExtractSymbols(parsers, filePath, code) { /** * Parse a single file and return normalized symbols. - * - * @param {string} filePath Absolute path to the file. - * @param {string} source Source code string. - * @param {object} [opts] Options: { engine: 'native'|'wasm'|'auto' } - * @returns {Promise<{definitions, calls, imports, classes, exports}|null>} */ -export async function parseFileAuto(filePath, source, opts = {}) { +export async function parseFileAuto( + filePath: string, + source: string, + opts: { + engine?: EngineMode; + nativeEngine?: any; + parsers?: Map; + rootDir?: string; + aliases?: any; + dataflow?: boolean; + ast?: boolean; + } = {}, +): Promise { const { native } = resolveEngine(opts); if (native) { @@ -474,15 +494,22 @@ export async function parseFileAuto(filePath, source, opts = {}) { /** * Parse multiple files in bulk and return a Map. - * - * @param {string[]} filePaths Absolute paths to files. - * @param {string} rootDir Project root for computing relative paths. - * @param {object} [opts] Options: { engine: 'native'|'wasm'|'auto' } - * @returns {Promise>} */ -export async function parseFilesAuto(filePaths, rootDir, opts = {}) { +export async function parseFilesAuto( + filePaths: string[], + rootDir: string, + opts: { + engine?: EngineMode; + nativeEngine?: any; + parsers?: Map; + aliases?: any; + signal?: AbortSignal; + dataflow?: boolean; + ast?: boolean; + } = {}, +): Promise> { const { native } = resolveEngine(opts); - const result = new Map(); + const result: Map = new Map(); if (native) { const nativeResults = native.parseFiles( @@ -491,7 +518,7 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) { !!opts.dataflow, opts.ast !== false, ); - const needsTypeMap = []; + const needsTypeMap: { filePath: string; relPath: string }[] = []; for (const r of nativeResults) { if (!r) continue; const patched = patchNativeResult(r); @@ -510,17 +537,19 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) { if (tsFiles.length > 0) { const parsers = await createParsers(); for (const { filePath, relPath } of tsFiles) { - let extracted; + let extracted: { symbols: ExtractorOutput; tree: any; langId: string } | null | undefined; try { const code = fs.readFileSync(filePath, 'utf-8'); extracted = wasmExtractSymbols(parsers, filePath, code); if (extracted?.symbols?.typeMap) { - const symbols = result.get(relPath); - symbols.typeMap = + const symbols = result.get(relPath)!; + (symbols as any).typeMap = extracted.symbols.typeMap instanceof Map ? extracted.symbols.typeMap - : new Map(extracted.symbols.typeMap.map((e) => [e.name, e.typeName])); - symbols._typeMapBackfilled = true; + : new Map( + (extracted.symbols.typeMap as any).map((e: any) => [e.name, e.typeName]), + ); + (symbols as any)._typeMapBackfilled = true; } } catch { /* skip — typeMap is a best-effort backfill */ @@ -541,10 +570,10 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) { // WASM path const parsers = await createParsers(); for (const filePath of filePaths) { - let code; + let code: string; try { code = fs.readFileSync(filePath, 'utf-8'); - } catch (err) { + } catch (err: any) { warn(`Skipping ${path.relative(rootDir, filePath)}: ${err.message}`); continue; } @@ -552,7 +581,7 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) { if (extracted) { const relPath = path.relative(rootDir, filePath).split(path.sep).join('/'); extracted.symbols._tree = extracted.tree; - extracted.symbols._langId = extracted.langId; + extracted.symbols._langId = extracted.langId as any; extracted.symbols._lineCount = code.split('\n').length; result.set(relPath, extracted.symbols); } @@ -562,13 +591,13 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) { /** * Report which engine is active. - * - * @param {object} [opts] Options: { engine: 'native'|'wasm'|'auto' } - * @returns {{ name: 'native'|'wasm', version: string|null }} */ -export function getActiveEngine(opts = {}) { +export function getActiveEngine(opts: { engine?: EngineMode; nativeEngine?: any } = {}): { + name: string; + version: string | null; +} { const { name, native } = resolveEngine(opts); - let version = native + let version: string | null = native ? typeof native.engineVersion === 'function' ? native.engineVersion() : null @@ -578,7 +607,7 @@ export function getActiveEngine(opts = {}) { if (native) { try { version = getNativePackageVersion() ?? version; - } catch (e) { + } catch (e: any) { debug(`getNativePackageVersion failed: ${e.message}`); } } @@ -589,7 +618,7 @@ export function getActiveEngine(opts = {}) { * Create a native ParseTreeCache for incremental parsing. * Returns null if the native engine is unavailable (WASM fallback). */ -export function createParseTreeCache() { +export function createParseTreeCache(): any { const native = loadNative(); if (!native || !native.ParseTreeCache) return null; return new native.ParseTreeCache(); @@ -597,14 +626,19 @@ export function createParseTreeCache() { /** * Parse a file incrementally using the cache, or fall back to full parse. - * - * @param {object|null} cache ParseTreeCache instance (or null for full parse) - * @param {string} filePath Absolute path to the file - * @param {string} source Source code string - * @param {object} [opts] Options forwarded to parseFileAuto on fallback - * @returns {Promise<{definitions, calls, imports, classes, exports}|null>} */ -export async function parseFileIncremental(cache, filePath, source, opts = {}) { +export async function parseFileIncremental( + cache: any, + filePath: string, + source: string, + opts: { + engine?: EngineMode; + nativeEngine?: any; + parsers?: Map; + rootDir?: string; + aliases?: any; + } = {}, +): Promise { if (cache) { const result = cache.parseFile(filePath, source); if (!result) return null; diff --git a/src/extractors/csharp.js b/src/extractors/csharp.ts similarity index 82% rename from src/extractors/csharp.js rename to src/extractors/csharp.ts index 520d7d86..8308abbb 100644 --- a/src/extractors/csharp.js +++ b/src/extractors/csharp.ts @@ -1,10 +1,18 @@ +import type { + Call, + ClassRelation, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; /** * Extract symbols from C# files. */ -export function extractCSharpSymbols(tree, _filePath) { - const ctx = { +export function extractCSharpSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -19,7 +27,7 @@ export function extractCSharpSymbols(tree, _filePath) { return ctx; } -function walkCSharpNode(node, ctx) { +function walkCSharpNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'class_declaration': handleCsClassDecl(node, ctx); @@ -56,12 +64,15 @@ function walkCSharpNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkCSharpNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkCSharpNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleCsClassDecl(node, ctx) { +function handleCsClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const classChildren = extractCSharpClassFields(node); @@ -75,7 +86,7 @@ function handleCsClassDecl(node, ctx) { extractCSharpBaseTypes(node, nameNode.text, ctx.classes); } -function handleCsStructDecl(node, ctx) { +function handleCsStructDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const structChildren = extractCSharpClassFields(node); @@ -89,7 +100,7 @@ function handleCsStructDecl(node, ctx) { extractCSharpBaseTypes(node, nameNode.text, ctx.classes); } -function handleCsRecordDecl(node, ctx) { +function handleCsRecordDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -101,7 +112,7 @@ function handleCsRecordDecl(node, ctx) { extractCSharpBaseTypes(node, nameNode.text, ctx.classes); } -function handleCsInterfaceDecl(node, ctx) { +function handleCsInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -129,7 +140,7 @@ function handleCsInterfaceDecl(node, ctx) { } } -function handleCsEnumDecl(node, ctx) { +function handleCsEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const enumChildren = extractCSharpEnumMembers(node); @@ -142,7 +153,7 @@ function handleCsEnumDecl(node, ctx) { }); } -function handleCsMethodDecl(node, ctx) { +function handleCsMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { // Skip interface methods already emitted by handleCsInterfaceDecl if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); @@ -160,7 +171,7 @@ function handleCsMethodDecl(node, ctx) { }); } -function handleCsConstructorDecl(node, ctx) { +function handleCsConstructorDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentType = findCSharpParentType(node); @@ -176,7 +187,7 @@ function handleCsConstructorDecl(node, ctx) { }); } -function handleCsPropertyDecl(node, ctx) { +function handleCsPropertyDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentType = findCSharpParentType(node); @@ -190,14 +201,14 @@ function handleCsPropertyDecl(node, ctx) { }); } -function handleCsUsingDirective(node, ctx) { +function handleCsUsingDirective(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name') || findChild(node, 'qualified_name') || findChild(node, 'identifier'); if (!nameNode) return; const fullPath = nameNode.text; - const lastName = fullPath.split('.').pop(); + const lastName = fullPath.split('.').pop() ?? fullPath; ctx.imports.push({ source: fullPath, names: [lastName], @@ -206,7 +217,7 @@ function handleCsUsingDirective(node, ctx) { }); } -function handleCsInvocationExpr(node, ctx) { +function handleCsInvocationExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function') || node.child(0); if (!fn) return; if (fn.type === 'identifier') { @@ -215,7 +226,7 @@ function handleCsInvocationExpr(node, ctx) { const name = fn.childForFieldName('name'); if (name) { const expr = fn.childForFieldName('expression'); - const call = { name: name.text, line: node.startPosition.row + 1 }; + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; if (expr) call.receiver = expr.text; ctx.calls.push(call); } @@ -225,7 +236,7 @@ function handleCsInvocationExpr(node, ctx) { } } -function handleCsObjectCreation(node, ctx) { +function handleCsObjectCreation(node: TreeSitterNode, ctx: ExtractorOutput): void { const typeNode = node.childForFieldName('type'); if (!typeNode) return; const typeName = @@ -235,7 +246,7 @@ function handleCsObjectCreation(node, ctx) { if (typeName) ctx.calls.push({ name: typeName, line: node.startPosition.row + 1 }); } -function findCSharpParentType(node) { +function findCSharpParentType(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if ( @@ -255,8 +266,8 @@ function findCSharpParentType(node) { // ── Child extraction helpers ──────────────────────────────────────────────── -function extractCSharpParameters(paramListNode) { - const params = []; +function extractCSharpParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] { + const params: SubDeclaration[] = []; if (!paramListNode) return params; for (let i = 0; i < paramListNode.childCount; i++) { const param = paramListNode.child(i); @@ -269,8 +280,8 @@ function extractCSharpParameters(paramListNode) { return params; } -function extractCSharpClassFields(classNode) { - const fields = []; +function extractCSharpClassFields(classNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list'); if (!body) return fields; for (let i = 0; i < body.childCount; i++) { @@ -295,8 +306,8 @@ function extractCSharpClassFields(classNode) { return fields; } -function extractCSharpEnumMembers(enumNode) { - const constants = []; +function extractCSharpEnumMembers(enumNode: TreeSitterNode): SubDeclaration[] { + const constants: SubDeclaration[] = []; const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_member_declaration_list'); if (!body) return constants; @@ -313,11 +324,15 @@ function extractCSharpEnumMembers(enumNode) { // ── Type map extraction ────────────────────────────────────────────────────── -function extractCSharpTypeMap(node, ctx) { +function extractCSharpTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void { extractCSharpTypeMapDepth(node, ctx, 0); } -function extractCSharpTypeMapDepth(node, ctx, depth) { +function extractCSharpTypeMapDepth( + node: TreeSitterNode, + ctx: ExtractorOutput, + depth: number, +): void { if (depth >= 200) return; // local_declaration_statement → variable_declaration → type + variable_declarator(s) @@ -331,7 +346,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) { if (child && child.type === 'variable_declarator') { const nameNode = child.childForFieldName('name') || child.child(0); if (nameNode && nameNode.type === 'identifier') { - ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); + ctx.typeMap?.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } } @@ -345,7 +360,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) { const nameNode = node.childForFieldName('name'); if (typeNode && nameNode) { const typeName = extractCSharpTypeName(typeNode); - if (typeName) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); + if (typeName) ctx.typeMap?.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } @@ -355,7 +370,7 @@ function extractCSharpTypeMapDepth(node, ctx, depth) { } } -function extractCSharpTypeName(typeNode) { +function extractCSharpTypeName(typeNode: TreeSitterNode): string | null { if (!typeNode) return null; const t = typeNode.type; if (t === 'identifier' || t === 'qualified_name') return typeNode.text; @@ -377,8 +392,8 @@ function extractCSharpTypeName(typeNode) { * base classes from interfaces in the base_list, so we fix it up here using the * definitions collected during the walk. */ -function reclassifyCSharpImplements(ctx) { - const interfaceNames = new Set(); +function reclassifyCSharpImplements(ctx: ExtractorOutput): void { + const interfaceNames = new Set(); for (const def of ctx.definitions) { if (def.kind === 'interface') interfaceNames.add(def.name); } @@ -390,7 +405,11 @@ function reclassifyCSharpImplements(ctx) { } } -function extractCSharpBaseTypes(node, className, classes) { +function extractCSharpBaseTypes( + node: TreeSitterNode, + className: string, + classes: ClassRelation[], +): void { // tree-sitter-c-sharp exposes base_list as a child node type, not a field const baseList = node.childForFieldName('bases') || findChild(node, 'base_list'); if (!baseList) return; diff --git a/src/extractors/go.js b/src/extractors/go.ts similarity index 82% rename from src/extractors/go.js rename to src/extractors/go.ts index 64d319d0..b31d50c3 100644 --- a/src/extractors/go.js +++ b/src/extractors/go.ts @@ -1,10 +1,18 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, + TypeMapEntry, +} from '../types.js'; import { findChild, goVisibility, nodeEndLine } from './helpers.js'; /** * Extract symbols from Go files. */ -export function extractGoSymbols(tree, _filePath) { - const ctx = { +export function extractGoSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -19,7 +27,7 @@ export function extractGoSymbols(tree, _filePath) { return ctx; } -function walkGoNode(node, ctx) { +function walkGoNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'function_declaration': handleGoFuncDecl(node, ctx); @@ -41,12 +49,15 @@ function walkGoNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkGoNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleGoFuncDecl(node, ctx) { +function handleGoFuncDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const params = extractGoParameters(node.childForFieldName('parameters')); @@ -60,11 +71,11 @@ function handleGoFuncDecl(node, ctx) { }); } -function handleGoMethodDecl(node, ctx) { +function handleGoMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); const receiver = node.childForFieldName('receiver'); if (!nameNode) return; - let receiverType = null; + let receiverType: string | null = null; if (receiver) { for (let i = 0; i < receiver.childCount; i++) { const param = receiver.child(i); @@ -89,7 +100,7 @@ function handleGoMethodDecl(node, ctx) { }); } -function handleGoTypeDecl(node, ctx) { +function handleGoTypeDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { for (let i = 0; i < node.childCount; i++) { const spec = node.child(i); if (!spec || spec.type !== 'type_spec') continue; @@ -138,7 +149,7 @@ function handleGoTypeDecl(node, ctx) { } } -function handleGoImportDecl(node, ctx) { +function handleGoImportDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; @@ -156,12 +167,12 @@ function handleGoImportDecl(node, ctx) { } } -function extractGoImportSpec(spec, ctx) { +function extractGoImportSpec(spec: TreeSitterNode, ctx: ExtractorOutput): void { const pathNode = spec.childForFieldName('path'); if (pathNode) { const importPath = pathNode.text.replace(/"/g, ''); const nameNode = spec.childForFieldName('name'); - const alias = nameNode ? nameNode.text : importPath.split('/').pop(); + const alias = nameNode ? nameNode.text : (importPath.split('/').pop() ?? importPath); ctx.imports.push({ source: importPath, names: [alias], @@ -171,7 +182,7 @@ function extractGoImportSpec(spec, ctx) { } } -function handleGoConstDecl(node, ctx) { +function handleGoConstDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { for (let i = 0; i < node.childCount; i++) { const spec = node.child(i); if (!spec || spec.type !== 'const_spec') continue; @@ -187,7 +198,7 @@ function handleGoConstDecl(node, ctx) { } } -function handleGoCallExpr(node, ctx) { +function handleGoCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; if (fn.type === 'identifier') { @@ -196,7 +207,7 @@ function handleGoCallExpr(node, ctx) { const field = fn.childForFieldName('field'); if (field) { const operand = fn.childForFieldName('operand'); - const call = { name: field.text, line: node.startPosition.row + 1 }; + const call: Call = { name: field.text, line: node.startPosition.row + 1 }; if (operand) call.receiver = operand.text; ctx.calls.push(call); } @@ -205,18 +216,23 @@ function handleGoCallExpr(node, ctx) { // ── Type map extraction ───────────────────────────────────────────────────── -function extractGoTypeMap(node, ctx) { +function extractGoTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void { extractGoTypeMapDepth(node, ctx, 0); } -function setIfHigher(typeMap, name, type, confidence) { +function setIfHigher( + typeMap: Map, + name: string, + type: string, + confidence: number, +): void { const existing = typeMap.get(name); if (!existing || confidence > existing.confidence) { typeMap.set(name, { type, confidence }); } } -function extractGoTypeMapDepth(node, ctx, depth) { +function extractGoTypeMapDepth(node: TreeSitterNode, ctx: ExtractorOutput, depth: number): void { if (depth >= 200) return; // var x MyType = ... or var x, y MyType → var_declaration > var_spec (confidence 0.9) @@ -228,7 +244,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'identifier') { - setIfHigher(ctx.typeMap, child.text, typeName, 0.9); + if (ctx.typeMap) setIfHigher(ctx.typeMap, child.text, typeName, 0.9); } } } @@ -244,7 +260,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'identifier') { - setIfHigher(ctx.typeMap, child.text, typeName, 0.9); + if (ctx.typeMap) setIfHigher(ctx.typeMap, child.text, typeName, 0.9); } } } @@ -260,7 +276,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { const lefts = left.type === 'expression_list' ? Array.from({ length: left.childCount }, (_, i) => left.child(i)).filter( - (c) => c?.type === 'identifier', + (c): c is TreeSitterNode => c?.type === 'identifier', ) : left.type === 'identifier' ? [left] @@ -268,7 +284,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { const rights = right.type === 'expression_list' ? Array.from({ length: right.childCount }, (_, i) => right.child(i)).filter( - (c) => c?.isNamed, + (c): c is TreeSitterNode => !!c?.type, ) : [right]; @@ -282,7 +298,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { const typeNode = rhs.childForFieldName('type'); if (typeNode) { const typeName = extractGoTypeName(typeNode); - if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); + if (typeName && ctx.typeMap) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); } } // x := &Struct{...} — address-of composite literal (confidence 1.0) @@ -292,7 +308,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { const typeNode = operand.childForFieldName('type'); if (typeNode) { const typeName = extractGoTypeName(typeNode); - if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); + if (typeName && ctx.typeMap) setIfHigher(ctx.typeMap, varNode.text, typeName, 1.0); } } } @@ -303,11 +319,11 @@ function extractGoTypeMapDepth(node, ctx, depth) { const field = fn.childForFieldName('field'); if (field?.text.startsWith('New')) { const typeName = field.text.slice(3); - if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); + if (typeName && ctx.typeMap) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); } } else if (fn && fn.type === 'identifier' && fn.text.startsWith('New')) { const typeName = fn.text.slice(3); - if (typeName) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); + if (typeName && ctx.typeMap) setIfHigher(ctx.typeMap, varNode.text, typeName, 0.7); } } } @@ -320,7 +336,7 @@ function extractGoTypeMapDepth(node, ctx, depth) { } } -function extractGoTypeName(typeNode) { +function extractGoTypeName(typeNode: TreeSitterNode): string | null { if (!typeNode) return null; const t = typeNode.type; if (t === 'type_identifier' || t === 'identifier') return typeNode.text; @@ -344,8 +360,8 @@ function extractGoTypeName(typeNode) { // ── Child extraction helpers ──────────────────────────────────────────────── -function extractGoParameters(paramListNode) { - const params = []; +function extractGoParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] { + const params: SubDeclaration[] = []; if (!paramListNode) return params; for (let i = 0; i < paramListNode.childCount; i++) { const param = paramListNode.child(i); @@ -368,14 +384,14 @@ function extractGoParameters(paramListNode) { * if it has methods matching every method declared in the interface. * This performs file-local matching (cross-file matching requires build-edges). */ -function matchGoStructuralInterfaces(ctx) { - const interfaceMethods = new Map(); - const structMethods = new Map(); - const structLines = new Map(); +function matchGoStructuralInterfaces(ctx: ExtractorOutput): void { + const interfaceMethods = new Map>(); + const structMethods = new Map>(); + const structLines = new Map(); // Collect interface and struct definitions - const interfaceNames = new Set(); - const structNames = new Set(); + const interfaceNames = new Set(); + const structNames = new Set(); for (const def of ctx.definitions) { if (def.kind === 'interface') interfaceNames.add(def.name); if (def.kind === 'struct') { @@ -393,11 +409,11 @@ function matchGoStructuralInterfaces(ctx) { if (interfaceNames.has(receiver)) { if (!interfaceMethods.has(receiver)) interfaceMethods.set(receiver, new Set()); - interfaceMethods.get(receiver).add(method); + interfaceMethods.get(receiver)?.add(method); } if (structNames.has(receiver)) { if (!structMethods.has(receiver)) structMethods.set(receiver, new Set()); - structMethods.get(receiver).add(method); + structMethods.get(receiver)?.add(method); } } @@ -419,8 +435,8 @@ function matchGoStructuralInterfaces(ctx) { } } -function extractStructFields(structTypeNode) { - const fields = []; +function extractStructFields(structTypeNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; const fieldList = findChild(structTypeNode, 'field_declaration_list'); if (!fieldList) return fields; for (let i = 0; i < fieldList.childCount; i++) { diff --git a/src/extractors/hcl.js b/src/extractors/hcl.ts similarity index 51% rename from src/extractors/hcl.js rename to src/extractors/hcl.ts index 8b13651f..a37792f9 100644 --- a/src/extractors/hcl.js +++ b/src/extractors/hcl.ts @@ -1,10 +1,18 @@ +import type { + Definition, + ExtractorOutput, + Import, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { nodeEndLine } from './helpers.js'; /** * Extract symbols from HCL (Terraform) files. */ -export function extractHCLSymbols(tree, _filePath) { - const ctx = { definitions: [], imports: [] }; +export function extractHCLSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: { definitions: Definition[]; imports: Import[] } = { definitions: [], imports: [] }; walkHclNode(tree.rootNode, ctx); return { @@ -13,39 +21,53 @@ export function extractHCLSymbols(tree, _filePath) { imports: ctx.imports, classes: [], exports: [], - }; + typeMap: new Map(), + } as ExtractorOutput; } -function walkHclNode(node, ctx) { +function walkHclNode( + node: TreeSitterNode, + ctx: { definitions: Definition[]; imports: Import[] }, +): void { if (node.type === 'block') { handleHclBlock(node, ctx); } - for (let i = 0; i < node.childCount; i++) walkHclNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkHclNode(child, ctx); + } } -function handleHclBlock(node, ctx) { - const children = []; - for (let i = 0; i < node.childCount; i++) children.push(node.child(i)); +function handleHclBlock( + node: TreeSitterNode, + ctx: { definitions: Definition[]; imports: Import[] }, +): void { + const children: TreeSitterNode[] = []; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) children.push(child); + } const identifiers = children.filter((c) => c.type === 'identifier'); const strings = children.filter((c) => c.type === 'string_lit'); - if (identifiers.length === 0) return; - const blockType = identifiers[0].text; + const firstIdent = identifiers[0]; + if (!firstIdent) return; + const blockType = firstIdent.text; const name = resolveHclBlockName(blockType, strings); if (name) { - let blockChildren; + let blockChildren: SubDeclaration[] | undefined; if (blockType === 'variable' || blockType === 'output') { blockChildren = extractHclAttributes(children); } ctx.definitions.push({ name, - kind: blockType, + kind: blockType as Definition['kind'], line: node.startPosition.row + 1, endLine: nodeEndLine(node), - children: blockChildren?.length > 0 ? blockChildren : undefined, + children: blockChildren?.length ? blockChildren : undefined, }); } @@ -54,30 +76,29 @@ function handleHclBlock(node, ctx) { } } -function resolveHclBlockName(blockType, strings) { - if (blockType === 'resource' && strings.length >= 2) { - return `${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; +function resolveHclBlockName(blockType: string, strings: TreeSitterNode[]): string { + const s0 = strings[0]; + const s1 = strings[1]; + if (blockType === 'resource' && s0 && s1) { + return `${s0.text.replace(/"/g, '')}.${s1.text.replace(/"/g, '')}`; } - if (blockType === 'data' && strings.length >= 2) { - return `data.${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; + if (blockType === 'data' && s0 && s1) { + return `data.${s0.text.replace(/"/g, '')}.${s1.text.replace(/"/g, '')}`; } - if ( - (blockType === 'variable' || blockType === 'output' || blockType === 'module') && - strings.length >= 1 - ) { - return `${blockType}.${strings[0].text.replace(/"/g, '')}`; + if ((blockType === 'variable' || blockType === 'output' || blockType === 'module') && s0) { + return `${blockType}.${s0.text.replace(/"/g, '')}`; } if (blockType === 'locals') return 'locals'; if (blockType === 'terraform' || blockType === 'provider') { let name = blockType; - if (strings.length >= 1) name += `.${strings[0].text.replace(/"/g, '')}`; + if (s0) name += `.${s0.text.replace(/"/g, '')}`; return name; } return ''; } -function extractHclAttributes(children) { - const attrs = []; +function extractHclAttributes(children: TreeSitterNode[]): SubDeclaration[] { + const attrs: SubDeclaration[] = []; const body = children.find((c) => c.type === 'body'); if (!body) return attrs; for (let j = 0; j < body.childCount; j++) { @@ -92,7 +113,11 @@ function extractHclAttributes(children) { return attrs; } -function extractHclModuleSource(children, _node, ctx) { +function extractHclModuleSource( + children: TreeSitterNode[], + _node: TreeSitterNode, + ctx: { definitions: Definition[]; imports: Import[] }, +): void { const body = children.find((c) => c.type === 'body'); if (!body) return; for (let i = 0; i < body.childCount; i++) { diff --git a/src/extractors/helpers.js b/src/extractors/helpers.ts similarity index 61% rename from src/extractors/helpers.js rename to src/extractors/helpers.ts index 1f8c7d22..be710d18 100644 --- a/src/extractors/helpers.js +++ b/src/extractors/helpers.ts @@ -1,11 +1,13 @@ -export function nodeEndLine(node) { +import type { TreeSitterNode } from '../types.js'; + +export function nodeEndLine(node: TreeSitterNode): number { return node.endPosition.row + 1; } -export function findChild(node, type) { +export function findChild(node: TreeSitterNode, type: string): TreeSitterNode | null { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); - if (child.type === type) return child; + if (child && child.type === type) return child; } return null; } @@ -17,18 +19,18 @@ export function findChild(node, type) { * @param {Set} [modifierTypes] - node types that indicate modifiers * @returns {'public'|'private'|'protected'|undefined} */ -const DEFAULT_MODIFIER_TYPES = new Set([ +const DEFAULT_MODIFIER_TYPES: Set = new Set([ 'modifiers', 'modifier', 'visibility_modifier', 'accessibility_modifier', ]); -const VISIBILITY_KEYWORDS = new Set(['public', 'private', 'protected']); +const VISIBILITY_KEYWORDS: Set = new Set(['public', 'private', 'protected']); /** * Python convention: __name → private, _name → protected, else undefined. */ -export function pythonVisibility(name) { +export function pythonVisibility(name: string): 'public' | 'private' | 'protected' | undefined { if (name.startsWith('__') && name.endsWith('__')) return undefined; // dunder — public if (name.startsWith('__')) return 'private'; if (name.startsWith('_')) return 'protected'; @@ -38,20 +40,20 @@ export function pythonVisibility(name) { /** * Go convention: uppercase first letter → public, lowercase → private. */ -export function goVisibility(name) { +export function goVisibility(name: string): 'public' | 'private' | 'protected' | undefined { if (!name) return undefined; // Strip receiver prefix (e.g., "Receiver.Method" → check "Method") - const bare = name.includes('.') ? name.split('.').pop() : name; + const bare = name.includes('.') ? (name.split('.').pop() ?? name) : name; if (!bare) return undefined; - return bare[0] === bare[0].toUpperCase() && bare[0] !== bare[0].toLowerCase() - ? 'public' - : 'private'; + const first = bare[0]; + if (!first) return undefined; + return first === first.toUpperCase() && first !== first.toLowerCase() ? 'public' : 'private'; } /** * Rust: check for `visibility_modifier` child (pub, pub(crate), etc.). */ -export function rustVisibility(node) { +export function rustVisibility(node: TreeSitterNode): 'public' | 'private' { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; @@ -62,19 +64,22 @@ export function rustVisibility(node) { return 'private'; } -export function extractModifierVisibility(node, modifierTypes = DEFAULT_MODIFIER_TYPES) { +export function extractModifierVisibility( + node: TreeSitterNode, + modifierTypes: Set = DEFAULT_MODIFIER_TYPES, +): 'public' | 'private' | 'protected' | undefined { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; // Direct keyword match (e.g., PHP visibility_modifier = "public") if (modifierTypes.has(child.type)) { const text = child.text; - if (VISIBILITY_KEYWORDS.has(text)) return text; + if (VISIBILITY_KEYWORDS.has(text)) return text as 'public' | 'private' | 'protected'; // C# 'private protected' — accessible to derived types in same assembly → protected if (text === 'private protected') return 'protected'; // Compound modifiers node (Java: "public static") — scan its text for a keyword for (const kw of VISIBILITY_KEYWORDS) { - if (text.includes(kw)) return kw; + if (text.includes(kw)) return kw as 'public' | 'private' | 'protected'; } } } diff --git a/src/extractors/index.js b/src/extractors/index.ts similarity index 100% rename from src/extractors/index.js rename to src/extractors/index.ts diff --git a/src/extractors/java.js b/src/extractors/java.ts similarity index 82% rename from src/extractors/java.js rename to src/extractors/java.ts index d44a0b8f..6277ff02 100644 --- a/src/extractors/java.js +++ b/src/extractors/java.ts @@ -1,10 +1,18 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, + TypeMapEntry, +} from '../types.js'; import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; /** * Extract symbols from Java files. */ -export function extractJavaSymbols(tree, _filePath) { - const ctx = { +export function extractJavaSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -17,7 +25,7 @@ export function extractJavaSymbols(tree, _filePath) { return ctx; } -function walkJavaNode(node, ctx) { +function walkJavaNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'class_declaration': handleJavaClassDecl(node, ctx); @@ -48,12 +56,15 @@ function walkJavaNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkJavaNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkJavaNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleJavaClassDecl(node, ctx) { +function handleJavaClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const classChildren = extractClassFields(node); @@ -93,7 +104,12 @@ function handleJavaClassDecl(node, ctx) { } } -function extractJavaInterfaces(interfaces, className, line, ctx) { +function extractJavaInterfaces( + interfaces: TreeSitterNode, + className: string, + line: number, + ctx: ExtractorOutput, +): void { for (let i = 0; i < interfaces.childCount; i++) { const child = interfaces.child(i); if ( @@ -122,7 +138,7 @@ function extractJavaInterfaces(interfaces, className, line, ctx) { } } -function handleJavaInterfaceDecl(node, ctx) { +function handleJavaInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -150,7 +166,7 @@ function handleJavaInterfaceDecl(node, ctx) { } } -function handleJavaEnumDecl(node, ctx) { +function handleJavaEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const enumChildren = extractEnumConstants(node); @@ -163,7 +179,7 @@ function handleJavaEnumDecl(node, ctx) { }); } -function handleJavaMethodDecl(node, ctx) { +function handleJavaMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { // Skip interface methods already emitted by handleJavaInterfaceDecl if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); @@ -181,7 +197,7 @@ function handleJavaMethodDecl(node, ctx) { }); } -function handleJavaConstructorDecl(node, ctx) { +function handleJavaConstructorDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentClass = findJavaParentClass(node); @@ -197,12 +213,12 @@ function handleJavaConstructorDecl(node, ctx) { }); } -function handleJavaImportDecl(node, ctx) { +function handleJavaImportDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && (child.type === 'scoped_identifier' || child.type === 'identifier')) { const fullPath = child.text; - const lastName = fullPath.split('.').pop(); + const lastName = fullPath.split('.').pop() ?? fullPath; ctx.imports.push({ source: fullPath, names: [lastName], @@ -217,16 +233,16 @@ function handleJavaImportDecl(node, ctx) { } } -function handleJavaMethodInvocation(node, ctx) { +function handleJavaMethodInvocation(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const obj = node.childForFieldName('object'); - const call = { name: nameNode.text, line: node.startPosition.row + 1 }; + const call: Call = { name: nameNode.text, line: node.startPosition.row + 1 }; if (obj) call.receiver = obj.text; ctx.calls.push(call); } -function handleJavaLocalVarDecl(node, ctx) { +function handleJavaLocalVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const typeNode = node.childForFieldName('type'); if (!typeNode) return; const typeName = typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text; @@ -235,19 +251,19 @@ function handleJavaLocalVarDecl(node, ctx) { const child = node.child(i); if (child?.type === 'variable_declarator') { const nameNode = child.childForFieldName('name'); - if (nameNode) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); + if (nameNode) ctx.typeMap?.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } } -function handleJavaObjectCreation(node, ctx) { +function handleJavaObjectCreation(node: TreeSitterNode, ctx: ExtractorOutput): void { const typeNode = node.childForFieldName('type'); if (!typeNode) return; const typeName = typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text; if (typeName) ctx.calls.push({ name: typeName, line: node.startPosition.row + 1 }); } -function findJavaParentClass(node) { +function findJavaParentClass(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if ( @@ -265,8 +281,11 @@ function findJavaParentClass(node) { // ── Child extraction helpers ──────────────────────────────────────────────── -function extractJavaParameters(paramListNode, typeMap) { - const params = []; +function extractJavaParameters( + paramListNode: TreeSitterNode | null, + typeMap?: Map, +): SubDeclaration[] { + const params: SubDeclaration[] = []; if (!paramListNode) return params; for (let i = 0; i < paramListNode.childCount; i++) { const param = paramListNode.child(i); @@ -289,8 +308,8 @@ function extractJavaParameters(paramListNode, typeMap) { return params; } -function extractClassFields(classNode) { - const fields = []; +function extractClassFields(classNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; const body = classNode.childForFieldName('body') || findChild(classNode, 'class_body'); if (!body) return fields; for (let i = 0; i < body.childCount; i++) { @@ -313,8 +332,8 @@ function extractClassFields(classNode) { return fields; } -function extractEnumConstants(enumNode) { - const constants = []; +function extractEnumConstants(enumNode: TreeSitterNode): SubDeclaration[] { + const constants: SubDeclaration[] = []; const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_body'); if (!body) return constants; for (let i = 0; i < body.childCount; i++) { diff --git a/src/extractors/javascript.js b/src/extractors/javascript.ts similarity index 80% rename from src/extractors/javascript.js rename to src/extractors/javascript.ts index 9b0dfd8a..a05a58d6 100644 --- a/src/extractors/javascript.js +++ b/src/extractors/javascript.ts @@ -1,8 +1,21 @@ import { debug } from '../infrastructure/logger.js'; +import type { + Call, + ClassRelation, + Definition, + Export, + ExtractorOutput, + Import, + SubDeclaration, + TreeSitterNode, + TreeSitterQuery, + TreeSitterTree, + TypeMapEntry, +} from '../types.js'; import { findChild, nodeEndLine } from './helpers.js'; /** Built-in globals that start with uppercase but are not user-defined types. */ -const BUILTIN_GLOBALS = new Set([ +const BUILTIN_GLOBALS: Set = new Set([ 'Math', 'JSON', 'Promise', @@ -63,64 +76,70 @@ const BUILTIN_GLOBALS = new Set([ * When a compiled tree-sitter Query is provided (from parser.js), * uses the fast query-based path. Falls back to manual tree walk otherwise. */ -export function extractSymbols(tree, _filePath, query) { +export function extractSymbols( + tree: TreeSitterTree, + _filePath: string, + query?: TreeSitterQuery, +): ExtractorOutput { if (query) return extractSymbolsQuery(tree, query); return extractSymbolsWalk(tree); } // ── Query-based extraction (fast path) ────────────────────────────────────── -function extractSymbolsQuery(tree, query) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exps = []; - const typeMap = new Map(); +function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): ExtractorOutput { + const definitions: Definition[] = []; + const calls: Call[] = []; + const imports: Import[] = []; + const classes: ClassRelation[] = []; + const exps: Export[] = []; + const typeMap: Map = new Map(); const matches = query.matches(tree.rootNode); for (const match of matches) { // Build capture lookup for this match (1-3 captures each, very fast) - const c = Object.create(null); + const c: Record = Object.create(null); for (const cap of match.captures) c[cap.name] = cap.node; - if (c.fn_node) { + if (c['fn_node']) { // function_declaration - const fnChildren = extractParameters(c.fn_node); + const fnChildren = extractParameters(c['fn_node']); definitions.push({ - name: c.fn_name.text, + name: c['fn_name']!.text, kind: 'function', - line: c.fn_node.startPosition.row + 1, - endLine: nodeEndLine(c.fn_node), + line: c['fn_node'].startPosition.row + 1, + endLine: nodeEndLine(c['fn_node']), children: fnChildren.length > 0 ? fnChildren : undefined, }); - } else if (c.varfn_name) { + } else if (c['varfn_name']) { // variable_declarator with arrow_function / function_expression - const declNode = c.varfn_name.parent?.parent; - const line = declNode ? declNode.startPosition.row + 1 : c.varfn_name.startPosition.row + 1; - const varFnChildren = extractParameters(c.varfn_value); + const declNode = c['varfn_name'].parent?.parent; + const line = declNode + ? declNode.startPosition.row + 1 + : c['varfn_name'].startPosition.row + 1; + const varFnChildren = extractParameters(c['varfn_value']!); definitions.push({ - name: c.varfn_name.text, + name: c['varfn_name'].text, kind: 'function', line, - endLine: nodeEndLine(c.varfn_value), + endLine: nodeEndLine(c['varfn_value']!), children: varFnChildren.length > 0 ? varFnChildren : undefined, }); - } else if (c.cls_node) { + } else if (c['cls_node']) { // class_declaration - const className = c.cls_name.text; - const startLine = c.cls_node.startPosition.row + 1; - const clsChildren = extractClassProperties(c.cls_node); + const className = c['cls_name']!.text; + const startLine = c['cls_node'].startPosition.row + 1; + const clsChildren = extractClassProperties(c['cls_node']); definitions.push({ name: className, kind: 'class', line: startLine, - endLine: nodeEndLine(c.cls_node), + endLine: nodeEndLine(c['cls_node']), children: clsChildren.length > 0 ? clsChildren : undefined, }); const heritage = - c.cls_node.childForFieldName('heritage') || findChild(c.cls_node, 'class_heritage'); + c['cls_node'].childForFieldName('heritage') || findChild(c['cls_node'], 'class_heritage'); if (heritage) { const superName = extractSuperclass(heritage); if (superName) classes.push({ name: className, extends: superName, line: startLine }); @@ -129,61 +148,61 @@ function extractSymbolsQuery(tree, query) { classes.push({ name: className, implements: iface, line: startLine }); } } - } else if (c.meth_node) { + } else if (c['meth_node']) { // method_definition - const methName = c.meth_name.text; - const parentClass = findParentClass(c.meth_node); + const methName = c['meth_name']!.text; + const parentClass = findParentClass(c['meth_node']); const fullName = parentClass ? `${parentClass}.${methName}` : methName; - const methChildren = extractParameters(c.meth_node); - const methVis = extractVisibility(c.meth_node); + const methChildren = extractParameters(c['meth_node']); + const methVis = extractVisibility(c['meth_node']); definitions.push({ name: fullName, kind: 'method', - line: c.meth_node.startPosition.row + 1, - endLine: nodeEndLine(c.meth_node), + line: c['meth_node'].startPosition.row + 1, + endLine: nodeEndLine(c['meth_node']), children: methChildren.length > 0 ? methChildren : undefined, visibility: methVis, }); - } else if (c.iface_node) { + } else if (c['iface_node']) { // interface_declaration (TS/TSX only) - const ifaceName = c.iface_name.text; + const ifaceName = c['iface_name']!.text; definitions.push({ name: ifaceName, kind: 'interface', - line: c.iface_node.startPosition.row + 1, - endLine: nodeEndLine(c.iface_node), + line: c['iface_node'].startPosition.row + 1, + endLine: nodeEndLine(c['iface_node']), }); const body = - c.iface_node.childForFieldName('body') || - findChild(c.iface_node, 'interface_body') || - findChild(c.iface_node, 'object_type'); + c['iface_node'].childForFieldName('body') || + findChild(c['iface_node'], 'interface_body') || + findChild(c['iface_node'], 'object_type'); if (body) extractInterfaceMethods(body, ifaceName, definitions); - } else if (c.type_node) { + } else if (c['type_node']) { // type_alias_declaration (TS/TSX only) definitions.push({ - name: c.type_name.text, + name: c['type_name']!.text, kind: 'type', - line: c.type_node.startPosition.row + 1, - endLine: nodeEndLine(c.type_node), + line: c['type_node'].startPosition.row + 1, + endLine: nodeEndLine(c['type_node']), }); - } else if (c.imp_node) { + } else if (c['imp_node']) { // import_statement - const isTypeOnly = c.imp_node.text.startsWith('import type'); - const modPath = c.imp_source.text.replace(/['"]/g, ''); - const names = extractImportNames(c.imp_node); + const isTypeOnly = c['imp_node'].text.startsWith('import type'); + const modPath = c['imp_source']!.text.replace(/['"]/g, ''); + const names = extractImportNames(c['imp_node']); imports.push({ source: modPath, names, - line: c.imp_node.startPosition.row + 1, + line: c['imp_node'].startPosition.row + 1, typeOnly: isTypeOnly, }); - } else if (c.exp_node) { + } else if (c['exp_node']) { // export_statement - const exportLine = c.exp_node.startPosition.row + 1; - const decl = c.exp_node.childForFieldName('declaration'); + const exportLine = c['exp_node'].startPosition.row + 1; + const decl = c['exp_node'].childForFieldName('declaration'); if (decl) { const declType = decl.type; - const kindMap = { + const kindMap: Record = { function_declaration: 'function', class_declaration: 'class', interface_declaration: 'interface', @@ -192,14 +211,15 @@ function extractSymbolsQuery(tree, query) { const kind = kindMap[declType]; if (kind) { const n = decl.childForFieldName('name'); - if (n) exps.push({ name: n.text, kind, line: exportLine }); + if (n) exps.push({ name: n.text, kind: kind as Export['kind'], line: exportLine }); } } - const source = c.exp_node.childForFieldName('source') || findChild(c.exp_node, 'string'); + const source = + c['exp_node'].childForFieldName('source') || findChild(c['exp_node'], 'string'); if (source && !decl) { const modPath = source.text.replace(/['"]/g, ''); - const reexportNames = extractImportNames(c.exp_node); - const nodeText = c.exp_node.text; + const reexportNames = extractImportNames(c['exp_node']); + const nodeText = c['exp_node'].text; const isWildcard = nodeText.includes('export *') || nodeText.includes('export*'); imports.push({ source: modPath, @@ -209,25 +229,25 @@ function extractSymbolsQuery(tree, query) { wildcardReexport: isWildcard && reexportNames.length === 0, }); } - } else if (c.callfn_node) { + } else if (c['callfn_node']) { // call_expression with identifier function calls.push({ - name: c.callfn_name.text, - line: c.callfn_node.startPosition.row + 1, + name: c['callfn_name']!.text, + line: c['callfn_node'].startPosition.row + 1, }); - } else if (c.callmem_node) { + } else if (c['callmem_node']) { // call_expression with member_expression function - const callInfo = extractCallInfo(c.callmem_fn, c.callmem_node); + const callInfo = extractCallInfo(c['callmem_fn']!, c['callmem_node']); if (callInfo) calls.push(callInfo); - const cbDef = extractCallbackDefinition(c.callmem_node, c.callmem_fn); + const cbDef = extractCallbackDefinition(c['callmem_node'], c['callmem_fn']); if (cbDef) definitions.push(cbDef); - } else if (c.callsub_node) { + } else if (c['callsub_node']) { // call_expression with subscript_expression function - const callInfo = extractCallInfo(c.callsub_fn, c.callsub_node); + const callInfo = extractCallInfo(c['callsub_fn']!, c['callsub_node']); if (callInfo) calls.push(callInfo); - } else if (c.assign_node) { + } else if (c['assign_node']) { // CommonJS: module.exports = require(...) / module.exports = { ...require(...) } - handleCommonJSAssignment(c.assign_left, c.assign_right, c.assign_node, imports); + handleCommonJSAssignment(c['assign_left']!, c['assign_right']!, c['assign_node'], imports); } } @@ -248,7 +268,7 @@ function extractSymbolsQuery(tree, query) { * The query-based fast path has no pattern for lexical_declaration/variable_declaration, * so constants are missed. This targeted walk fills that gap without a full tree traversal. */ -function extractConstantsWalk(rootNode, definitions) { +function extractConstantsWalk(rootNode: TreeSitterNode, definitions: Definition[]): void { for (let i = 0; i < rootNode.childCount; i++) { const node = rootNode.child(i); if (!node) continue; @@ -296,7 +316,7 @@ function extractConstantsWalk(rootNode, definitions) { * Query patterns match call_expression with identifier/member_expression/subscript_expression * functions, but import() has function type `import` which none of those patterns cover. */ -function extractDynamicImportsWalk(node, imports) { +function extractDynamicImportsWalk(node: TreeSitterNode, imports: Import[]): void { if (node.type === 'call_expression') { const fn = node.childForFieldName('function'); if (fn && fn.type === 'import') { @@ -322,11 +342,16 @@ function extractDynamicImportsWalk(node, imports) { } } for (let i = 0; i < node.childCount; i++) { - extractDynamicImportsWalk(node.child(i), imports); + extractDynamicImportsWalk(node.child(i)!, imports); } } -function handleCommonJSAssignment(left, right, node, imports) { +function handleCommonJSAssignment( + left: TreeSitterNode, + right: TreeSitterNode, + node: TreeSitterNode, + imports: Import[], +): void { if (!left || !right) return; const leftText = left.text; if (!leftText.startsWith('module.exports') && leftText !== 'exports') return; @@ -380,8 +405,8 @@ function handleCommonJSAssignment(left, right, node, imports) { // ── Manual tree walk (fallback when Query not available) ──────────────────── -function extractSymbolsWalk(tree) { - const ctx = { +function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -392,11 +417,11 @@ function extractSymbolsWalk(tree) { walkJavaScriptNode(tree.rootNode, ctx); // Populate typeMap for variables and parameter type annotations - extractTypeMapWalk(tree.rootNode, ctx.typeMap); + extractTypeMapWalk(tree.rootNode, ctx.typeMap!); return ctx; } -function walkJavaScriptNode(node, ctx) { +function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'function_declaration': handleFunctionDecl(node, ctx); @@ -435,13 +460,13 @@ function walkJavaScriptNode(node, ctx) { } for (let i = 0; i < node.childCount; i++) { - walkJavaScriptNode(node.child(i), ctx); + walkJavaScriptNode(node.child(i)!, ctx); } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleFunctionDecl(node, ctx) { +function handleFunctionDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (nameNode) { const fnChildren = extractParameters(node); @@ -455,7 +480,7 @@ function handleFunctionDecl(node, ctx) { } } -function handleClassDecl(node, ctx) { +function handleClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const className = nameNode.text; @@ -481,7 +506,7 @@ function handleClassDecl(node, ctx) { } } -function handleMethodDef(node, ctx) { +function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (nameNode) { const parentClass = findParentClass(node); @@ -499,7 +524,7 @@ function handleMethodDef(node, ctx) { } } -function handleInterfaceDecl(node, ctx) { +function handleInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -517,7 +542,7 @@ function handleInterfaceDecl(node, ctx) { } } -function handleTypeAliasDecl(node, ctx) { +function handleTypeAliasDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (nameNode) { ctx.definitions.push({ @@ -529,7 +554,7 @@ function handleTypeAliasDecl(node, ctx) { } } -function handleVariableDecl(node, ctx) { +function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const isConst = node.text.startsWith('const '); for (let i = 0; i < node.childCount; i++) { const declarator = node.child(i); @@ -565,10 +590,10 @@ function handleVariableDecl(node, ctx) { } } -function handleEnumDecl(node, ctx) { +function handleEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; - const enumChildren = []; + const enumChildren: SubDeclaration[] = []; const body = node.childForFieldName('body') || findChild(node, 'enum_body'); if (body) { for (let i = 0; i < body.childCount; i++) { @@ -595,7 +620,7 @@ function handleEnumDecl(node, ctx) { }); } -function handleCallExpr(node, ctx) { +function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; if (fn.type === 'import') { @@ -627,7 +652,7 @@ function handleCallExpr(node, ctx) { } } -function handleImportStmt(node, ctx) { +function handleImportStmt(node: TreeSitterNode, ctx: ExtractorOutput): void { const isTypeOnly = node.text.startsWith('import type'); const source = node.childForFieldName('source') || findChild(node, 'string'); if (source) { @@ -642,12 +667,12 @@ function handleImportStmt(node, ctx) { } } -function handleExportStmt(node, ctx) { +function handleExportStmt(node: TreeSitterNode, ctx: ExtractorOutput): void { const exportLine = node.startPosition.row + 1; const decl = node.childForFieldName('declaration'); if (decl) { const declType = decl.type; - const kindMap = { + const kindMap: Record = { function_declaration: 'function', class_declaration: 'class', interface_declaration: 'interface', @@ -656,7 +681,7 @@ function handleExportStmt(node, ctx) { const kind = kindMap[declType]; if (kind) { const n = decl.childForFieldName('name'); - if (n) ctx.exports.push({ name: n.text, kind, line: exportLine }); + if (n) ctx.exports.push({ name: n.text, kind: kind as Export['kind'], line: exportLine }); } } const source = node.childForFieldName('source') || findChild(node, 'string'); @@ -675,19 +700,19 @@ function handleExportStmt(node, ctx) { } } -function handleExpressionStmt(node, ctx) { +function handleExpressionStmt(node: TreeSitterNode, ctx: ExtractorOutput): void { const expr = node.child(0); if (expr && expr.type === 'assignment_expression') { const left = expr.childForFieldName('left'); const right = expr.childForFieldName('right'); - handleCommonJSAssignment(left, right, node, ctx.imports); + if (left && right) handleCommonJSAssignment(left, right, node, ctx.imports); } } // ── Child extraction helpers ──────────────────────────────────────────────── -function extractParameters(node) { - const params = []; +function extractParameters(node: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; const paramsNode = node.childForFieldName('parameters') || findChild(node, 'formal_parameters'); if (!paramsNode) return params; for (let i = 0; i < paramsNode.childCount; i++) { @@ -720,8 +745,8 @@ function extractParameters(node) { return params; } -function extractClassProperties(classNode) { - const props = []; +function extractClassProperties(classNode: TreeSitterNode): SubDeclaration[] { + const props: SubDeclaration[] = []; const body = classNode.childForFieldName('body') || findChild(classNode, 'class_body'); if (!body) return props; for (let i = 0; i < body.childCount; i++) { @@ -761,7 +786,7 @@ function extractClassProperties(classNode) { * Checks for TS access modifiers (public/private/protected) and JS private (#) fields. * Returns 'public' | 'private' | 'protected' | undefined. */ -function extractVisibility(node) { +function extractVisibility(node: TreeSitterNode): 'public' | 'private' | 'protected' | undefined { // Check for TS accessibility modifiers (accessibility_modifier child) for (let i = 0; i < node.childCount; i++) { const child = node.child(i); @@ -780,7 +805,7 @@ function extractVisibility(node) { return undefined; } -function isConstantValue(valueNode) { +function isConstantValue(valueNode: TreeSitterNode): boolean { if (!valueNode) return false; const t = valueNode.type; return ( @@ -802,7 +827,11 @@ function isConstantValue(valueNode) { // ── Shared helpers ────────────────────────────────────────────────────────── -function extractInterfaceMethods(bodyNode, interfaceName, definitions) { +function extractInterfaceMethods( + bodyNode: TreeSitterNode, + interfaceName: string, + definitions: Definition[], +): void { for (let i = 0; i < bodyNode.childCount; i++) { const child = bodyNode.child(i); if (!child) continue; @@ -820,8 +849,8 @@ function extractInterfaceMethods(bodyNode, interfaceName, definitions) { } } -function extractImplements(heritage) { - const interfaces = []; +function extractImplements(heritage: TreeSitterNode): string[] { + const interfaces: string[] = []; for (let i = 0; i < heritage.childCount; i++) { const child = heritage.child(i); if (!child) continue; @@ -842,8 +871,8 @@ function extractImplements(heritage) { return interfaces; } -function extractImplementsFromNode(node) { - const result = []; +function extractImplementsFromNode(node: TreeSitterNode): string[] { + const result: string[] = []; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; @@ -855,7 +884,7 @@ function extractImplementsFromNode(node) { // ── Type inference helpers ─────────────────────────────────────────────── -function extractSimpleTypeName(typeAnnotationNode) { +function extractSimpleTypeName(typeAnnotationNode: TreeSitterNode): string | null { if (!typeAnnotationNode) return null; for (let i = 0; i < typeAnnotationNode.childCount; i++) { const child = typeAnnotationNode.child(i); @@ -869,7 +898,7 @@ function extractSimpleTypeName(typeAnnotationNode) { return null; } -function extractNewExprTypeName(newExprNode) { +function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null { if (!newExprNode || newExprNode.type !== 'new_expression') return null; const ctor = newExprNode.childForFieldName('constructor') || newExprNode.child(1); if (!ctor) return null; @@ -891,15 +920,15 @@ function extractNewExprTypeName(newExprNode) { * * Higher-confidence entries take priority when the same variable is seen twice. */ -function extractTypeMapWalk(rootNode, typeMap) { - function setIfHigher(name, type, confidence) { +function extractTypeMapWalk(rootNode: TreeSitterNode, typeMap: Map): void { + function setIfHigher(name: string, type: string, confidence: number): void { const existing = typeMap.get(name); if (!existing || confidence > existing.confidence) { typeMap.set(name, { type, confidence }); } } - function walk(node, depth) { + function walk(node: TreeSitterNode, depth: number): void { if (depth >= 200) return; const t = node.type; if (t === 'variable_declarator') { @@ -924,7 +953,7 @@ function extractTypeMapWalk(rootNode, typeMap) { const obj = fn.childForFieldName('object'); if (obj && obj.type === 'identifier') { const objName = obj.text; - if (objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS.has(objName)) { + if (objName[0]! !== objName[0]!.toLowerCase() && !BUILTIN_GLOBALS.has(objName)) { setIfHigher(nameN.text, objName, 0.7); } } @@ -944,20 +973,20 @@ function extractTypeMapWalk(rootNode, typeMap) { } } for (let i = 0; i < node.childCount; i++) { - walk(node.child(i), depth + 1); + walk(node.child(i)!, depth + 1); } } walk(rootNode, 0); } -function extractReceiverName(objNode) { +function extractReceiverName(objNode: TreeSitterNode | null): string | undefined { if (!objNode) return undefined; const t = objNode.type; if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text; return objNode.text; } -function extractCallInfo(fn, callNode) { +function extractCallInfo(fn: TreeSitterNode, callNode: TreeSitterNode): Call | null { const fnType = fn.type; if (fnType === 'identifier') { return { name: fn.text, line: callNode.startPosition.row + 1 }; @@ -1016,7 +1045,7 @@ function extractCallInfo(fn, callNode) { return null; } -function findAnonymousCallback(argsNode) { +function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null { for (let i = 0; i < argsNode.childCount; i++) { const child = argsNode.child(i); if (child && (child.type === 'arrow_function' || child.type === 'function_expression')) { @@ -1026,7 +1055,7 @@ function findAnonymousCallback(argsNode) { return null; } -function findFirstStringArg(argsNode) { +function findFirstStringArg(argsNode: TreeSitterNode): string | null { for (let i = 0; i < argsNode.childCount; i++) { const child = argsNode.child(i); if (child && child.type === 'string') { @@ -1036,8 +1065,8 @@ function findFirstStringArg(argsNode) { return null; } -function walkCallChain(startNode, methodName) { - let current = startNode; +function walkCallChain(startNode: TreeSitterNode, methodName: string): TreeSitterNode | null { + let current: TreeSitterNode | null = startNode; while (current) { const curType = current.type; if (curType === 'call_expression') { @@ -1058,7 +1087,7 @@ function walkCallChain(startNode, methodName) { return null; } -const EXPRESS_METHODS = new Set([ +const EXPRESS_METHODS: Set = new Set([ 'get', 'post', 'put', @@ -1069,9 +1098,12 @@ const EXPRESS_METHODS = new Set([ 'all', 'use', ]); -const EVENT_METHODS = new Set(['on', 'once', 'addEventListener', 'addListener']); +const EVENT_METHODS: Set = new Set(['on', 'once', 'addEventListener', 'addListener']); -function extractCallbackDefinition(callNode, fn) { +function extractCallbackDefinition( + callNode: TreeSitterNode, + fn?: TreeSitterNode | null, +): Definition | null { if (!fn) fn = callNode.childForFieldName('function'); if (!fn || fn.type !== 'member_expression') return null; @@ -1086,14 +1118,14 @@ function extractCallbackDefinition(callNode, fn) { if (method === 'action') { const cb = findAnonymousCallback(args); if (!cb) return null; - const commandCall = walkCallChain(fn.childForFieldName('object'), 'command'); + const commandCall = walkCallChain(fn.childForFieldName('object')!, 'command'); if (!commandCall) return null; const cmdArgs = commandCall.childForFieldName('arguments') || findChild(commandCall, 'arguments'); if (!cmdArgs) return null; const cmdName = findFirstStringArg(cmdArgs); if (!cmdName) return null; - const firstWord = cmdName.split(/\s/)[0]; + const firstWord = cmdName.split(/\s/)[0]!; return { name: `command:${firstWord}`, kind: 'function', @@ -1133,9 +1165,9 @@ function extractCallbackDefinition(callNode, fn) { return null; } -function extractSuperclass(heritage) { +function extractSuperclass(heritage: TreeSitterNode): string | null { for (let i = 0; i < heritage.childCount; i++) { - const child = heritage.child(i); + const child = heritage.child(i)!; if (child.type === 'identifier') return child.text; if (child.type === 'member_expression') return child.text; const found = extractSuperclass(child); @@ -1144,7 +1176,7 @@ function extractSuperclass(heritage) { return null; } -function findParentClass(node) { +function findParentClass(node: TreeSitterNode): string | null { let current = node.parent; while (current) { const t = current.type; @@ -1157,9 +1189,9 @@ function findParentClass(node) { return null; } -function extractImportNames(node) { - const names = []; - function scan(n) { +function extractImportNames(node: TreeSitterNode): string[] { + const names: string[] = []; + function scan(n: TreeSitterNode): void { if (n.type === 'import_specifier' || n.type === 'export_specifier') { const nameNode = n.childForFieldName('name') || n.childForFieldName('alias'); if (nameNode) names.push(nameNode.text); @@ -1169,7 +1201,7 @@ function extractImportNames(node) { } else if (n.type === 'namespace_import') { names.push(n.text); } - for (let i = 0; i < n.childCount; i++) scan(n.child(i)); + for (let i = 0; i < n.childCount; i++) scan(n.child(i)!); } scan(node); return names; @@ -1186,7 +1218,7 @@ function extractImportNames(node) { * Walks up the AST from the call_expression to find the enclosing * variable_declarator and reads the name/object_pattern. */ -function extractDynamicImportNames(callNode) { +function extractDynamicImportNames(callNode: TreeSitterNode): string[] { // Walk up: call_expression → await_expression → variable_declarator let current = callNode.parent; // Skip await_expression wrapper if present @@ -1199,9 +1231,9 @@ function extractDynamicImportNames(callNode) { // const { a, b } = await import(...) → object_pattern if (nameNode.type === 'object_pattern') { - const names = []; + const names: string[] = []; for (let i = 0; i < nameNode.childCount; i++) { - const child = nameNode.child(i); + const child = nameNode.child(i)!; if (child.type === 'shorthand_property_identifier_pattern') { names.push(child.text); } else if (child.type === 'pair_pattern') { @@ -1221,9 +1253,9 @@ function extractDynamicImportNames(callNode) { // const [a, b] = await import(...) → array_pattern (rare but possible) if (nameNode.type === 'array_pattern') { - const names = []; + const names: string[] = []; for (let i = 0; i < nameNode.childCount; i++) { - const child = nameNode.child(i); + const child = nameNode.child(i)!; if (child.type === 'identifier') names.push(child.text); else if (child.type === 'rest_pattern') { const inner = child.child(0) || child.childForFieldName('name'); diff --git a/src/extractors/php.js b/src/extractors/php.ts similarity index 80% rename from src/extractors/php.js rename to src/extractors/php.ts index d59d1410..1219665b 100644 --- a/src/extractors/php.js +++ b/src/extractors/php.ts @@ -1,7 +1,14 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js'; -function extractPhpParameters(fnNode) { - const params = []; +function extractPhpParameters(fnNode: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'formal_parameters'); if (!paramsNode) return params; @@ -18,8 +25,8 @@ function extractPhpParameters(fnNode) { return params; } -function extractPhpClassChildren(classNode) { - const children = []; +function extractPhpClassChildren(classNode: TreeSitterNode): SubDeclaration[] { + const children: SubDeclaration[] = []; const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list'); if (!body) return children; for (let i = 0; i < body.childCount; i++) { @@ -57,8 +64,8 @@ function extractPhpClassChildren(classNode) { return children; } -function extractPhpEnumCases(enumNode) { - const children = []; +function extractPhpEnumCases(enumNode: TreeSitterNode): SubDeclaration[] { + const children: SubDeclaration[] = []; const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_declaration_list'); if (!body) return children; for (let i = 0; i < body.childCount; i++) { @@ -75,8 +82,8 @@ function extractPhpEnumCases(enumNode) { /** * Extract symbols from PHP files. */ -export function extractPHPSymbols(tree, _filePath) { - const ctx = { +export function extractPHPSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -90,7 +97,7 @@ export function extractPHPSymbols(tree, _filePath) { return ctx; } -function walkPhpNode(node, ctx) { +function walkPhpNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'function_definition': handlePhpFuncDef(node, ctx); @@ -127,12 +134,15 @@ function walkPhpNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkPhpNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkPhpNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handlePhpFuncDef(node, ctx) { +function handlePhpFuncDef(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const params = extractPhpParameters(node); @@ -145,7 +155,7 @@ function handlePhpFuncDef(node, ctx) { }); } -function handlePhpClassDecl(node, ctx) { +function handlePhpClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const classChildren = extractPhpClassChildren(node); @@ -185,7 +195,7 @@ function handlePhpClassDecl(node, ctx) { } } -function handlePhpInterfaceDecl(node, ctx) { +function handlePhpInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -213,7 +223,7 @@ function handlePhpInterfaceDecl(node, ctx) { } } -function handlePhpTraitDecl(node, ctx) { +function handlePhpTraitDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -224,7 +234,7 @@ function handlePhpTraitDecl(node, ctx) { }); } -function handlePhpEnumDecl(node, ctx) { +function handlePhpEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const enumChildren = extractPhpEnumCases(node); @@ -237,7 +247,7 @@ function handlePhpEnumDecl(node, ctx) { }); } -function handlePhpMethodDecl(node, ctx) { +function handlePhpMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { // Skip interface methods already emitted by handlePhpInterfaceDecl if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); @@ -255,14 +265,14 @@ function handlePhpMethodDecl(node, ctx) { }); } -function handlePhpNamespaceUse(node, ctx) { +function handlePhpNamespaceUse(node: TreeSitterNode, ctx: ExtractorOutput): void { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'namespace_use_clause') { const nameNode = findChild(child, 'qualified_name') || findChild(child, 'name'); if (nameNode) { const fullPath = nameNode.text; - const lastName = fullPath.split('\\').pop(); + const lastName = fullPath.split('\\').pop() ?? fullPath; const alias = child.childForFieldName('alias'); ctx.imports.push({ source: fullPath, @@ -274,7 +284,7 @@ function handlePhpNamespaceUse(node, ctx) { } if (child && (child.type === 'qualified_name' || child.type === 'name')) { const fullPath = child.text; - const lastName = fullPath.split('\\').pop(); + const lastName = fullPath.split('\\').pop() ?? fullPath; ctx.imports.push({ source: fullPath, names: [lastName], @@ -285,48 +295,51 @@ function handlePhpNamespaceUse(node, ctx) { } } -function handlePhpFuncCall(node, ctx) { +function handlePhpFuncCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function') || node.child(0); if (!fn) return; if (fn.type === 'name' || fn.type === 'identifier') { ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); } else if (fn.type === 'qualified_name') { const parts = fn.text.split('\\'); - ctx.calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 }); + ctx.calls.push({ name: parts[parts.length - 1] ?? fn.text, line: node.startPosition.row + 1 }); } } -function handlePhpMemberCall(node, ctx) { +function handlePhpMemberCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const name = node.childForFieldName('name'); if (!name) return; const obj = node.childForFieldName('object'); - const call = { name: name.text, line: node.startPosition.row + 1 }; + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; if (obj) call.receiver = obj.text; ctx.calls.push(call); } -function handlePhpScopedCall(node, ctx) { +function handlePhpScopedCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const name = node.childForFieldName('name'); if (!name) return; const scope = node.childForFieldName('scope'); - const call = { name: name.text, line: node.startPosition.row + 1 }; + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; if (scope) call.receiver = scope.text; ctx.calls.push(call); } -function handlePhpObjectCreation(node, ctx) { +function handlePhpObjectCreation(node: TreeSitterNode, ctx: ExtractorOutput): void { const classNode = node.child(1); if (classNode && (classNode.type === 'name' || classNode.type === 'qualified_name')) { const parts = classNode.text.split('\\'); - ctx.calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 }); + ctx.calls.push({ + name: parts[parts.length - 1] ?? classNode.text, + line: node.startPosition.row + 1, + }); } } -function extractPhpTypeMap(node, ctx) { +function extractPhpTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void { extractPhpTypeMapDepth(node, ctx, 0); } -function extractPhpTypeMapDepth(node, ctx, depth) { +function extractPhpTypeMapDepth(node: TreeSitterNode, ctx: ExtractorOutput, depth: number): void { if (depth >= 200) return; // Function/method parameters with type hints @@ -339,7 +352,7 @@ function extractPhpTypeMapDepth(node, ctx, depth) { const nameNode = node.childForFieldName('name') || findChild(node, 'variable_name'); if (typeNode && nameNode) { const typeName = extractPhpTypeName(typeNode); - if (typeName) ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 }); + if (typeName) ctx.typeMap?.set(nameNode.text, { type: typeName, confidence: 0.9 }); } } @@ -349,7 +362,7 @@ function extractPhpTypeMapDepth(node, ctx, depth) { } } -function extractPhpTypeName(typeNode) { +function extractPhpTypeName(typeNode: TreeSitterNode): string | null { if (!typeNode) return null; const t = typeNode.type; if (t === 'named_type' || t === 'name' || t === 'qualified_name') return typeNode.text; @@ -363,7 +376,7 @@ function extractPhpTypeName(typeNode) { return null; } -function findPHPParentClass(node) { +function findPHPParentClass(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if ( diff --git a/src/extractors/python.js b/src/extractors/python.ts similarity index 80% rename from src/extractors/python.js rename to src/extractors/python.ts index 416b9737..0443237d 100644 --- a/src/extractors/python.js +++ b/src/extractors/python.ts @@ -1,7 +1,15 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, + TypeMapEntry, +} from '../types.js'; import { findChild, nodeEndLine, pythonVisibility } from './helpers.js'; /** Built-in globals that start with uppercase but are not user-defined types. */ -const BUILTIN_GLOBALS_PY = new Set([ +const BUILTIN_GLOBALS_PY: Set = new Set([ // Uppercase builtins that would false-positive on the factory heuristic 'Exception', 'BaseException', @@ -47,8 +55,8 @@ const BUILTIN_GLOBALS_PY = new Set([ /** * Extract symbols from Python files. */ -export function extractPythonSymbols(tree, _filePath) { - const ctx = { +export function extractPythonSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -62,7 +70,7 @@ export function extractPythonSymbols(tree, _filePath) { return ctx; } -function walkPythonNode(node, ctx) { +function walkPythonNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'function_definition': handlePyFunctionDef(node, ctx); @@ -71,7 +79,10 @@ function walkPythonNode(node, ctx) { handlePyClassDef(node, ctx); break; case 'decorated_definition': - for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkPythonNode(child, ctx); + } return; case 'call': handlePyCall(node, ctx); @@ -87,15 +98,18 @@ function walkPythonNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkPythonNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handlePyFunctionDef(node, ctx) { +function handlePyFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; - const decorators = []; + const decorators: string[] = []; if (node.previousSibling && node.previousSibling.type === 'decorator') { decorators.push(node.previousSibling.text); } @@ -114,7 +128,7 @@ function handlePyFunctionDef(node, ctx) { }); } -function handlePyClassDef(node, ctx) { +function handlePyClassDef(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const clsChildren = extractPythonClassProperties(node); @@ -140,11 +154,11 @@ function handlePyClassDef(node, ctx) { } } -function handlePyCall(node, ctx) { +function handlePyCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; - let callName = null; - let receiver; + let callName: string | null = null; + let receiver: string | undefined; if (fn.type === 'identifier') callName = fn.text; else if (fn.type === 'attribute') { const attr = fn.childForFieldName('attribute'); @@ -153,14 +167,14 @@ function handlePyCall(node, ctx) { if (obj) receiver = obj.text; } if (callName) { - const call = { name: callName, line: node.startPosition.row + 1 }; + const call: Call = { name: callName, line: node.startPosition.row + 1 }; if (receiver) call.receiver = receiver; ctx.calls.push(call); } } -function handlePyImport(node, ctx) { - const names = []; +function handlePyImport(node: TreeSitterNode, ctx: ExtractorOutput): void { + const names: string[] = []; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && (child.type === 'dotted_name' || child.type === 'aliased_import')) { @@ -173,14 +187,14 @@ function handlePyImport(node, ctx) { } if (names.length > 0) ctx.imports.push({ - source: names[0], + source: names[0] ?? '', names, line: node.startPosition.row + 1, pythonImport: true, }); } -function handlePyExpressionStmt(node, ctx) { +function handlePyExpressionStmt(node: TreeSitterNode, ctx: ExtractorOutput): void { if (node.parent && node.parent.type === 'module') { const assignment = findChild(node, 'assignment'); if (assignment) { @@ -196,9 +210,9 @@ function handlePyExpressionStmt(node, ctx) { } } -function handlePyImportFrom(node, ctx) { +function handlePyImportFrom(node: TreeSitterNode, ctx: ExtractorOutput): void { let source = ''; - const names = []; + const names: string[] = []; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; @@ -218,8 +232,8 @@ function handlePyImportFrom(node, ctx) { // ── Python-specific helpers ───────────────────────────────────────────────── -function extractPythonParameters(fnNode) { - const params = []; +function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters'); if (!paramsNode) return params; for (let i = 0; i < paramsNode.childCount; i++) { @@ -254,9 +268,9 @@ function extractPythonParameters(fnNode) { return params; } -function extractPythonClassProperties(classNode) { - const props = []; - const seen = new Set(); +function extractPythonClassProperties(classNode: TreeSitterNode): SubDeclaration[] { + const props: SubDeclaration[] = []; + const seen = new Set(); const body = classNode.childForFieldName('body') || findChild(classNode, 'block'); if (!body) return props; @@ -308,7 +322,7 @@ function extractPythonClassProperties(classNode) { return props; } -function walkInitBody(bodyNode, seen, props) { +function walkInitBody(bodyNode: TreeSitterNode, seen: Set, props: SubDeclaration[]): void { for (let i = 0; i < bodyNode.childCount; i++) { const stmt = bodyNode.child(i); if (!stmt || stmt.type !== 'expression_statement') continue; @@ -330,18 +344,27 @@ function walkInitBody(bodyNode, seen, props) { } } -function extractPythonTypeMap(node, ctx) { +function extractPythonTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void { extractPythonTypeMapDepth(node, ctx, 0); } -function setIfHigherPy(typeMap, name, type, confidence) { +function setIfHigherPy( + typeMap: Map, + name: string, + type: string, + confidence: number, +): void { const existing = typeMap.get(name); if (!existing || confidence > existing.confidence) { typeMap.set(name, { type, confidence }); } } -function extractPythonTypeMapDepth(node, ctx, depth) { +function extractPythonTypeMapDepth( + node: TreeSitterNode, + ctx: ExtractorOutput, + depth: number, +): void { if (depth >= 200) return; // typed_parameter: identifier : type (confidence 0.9) @@ -351,7 +374,7 @@ function extractPythonTypeMapDepth(node, ctx, depth) { if (nameNode && nameNode.type === 'identifier' && typeNode) { const typeName = extractPythonTypeName(typeNode); if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') { - setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); + if (ctx.typeMap) setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); } } } @@ -363,7 +386,7 @@ function extractPythonTypeMapDepth(node, ctx, depth) { if (nameNode && nameNode.type === 'identifier' && typeNode) { const typeName = extractPythonTypeName(typeNode); if (typeName && nameNode.text !== 'self' && nameNode.text !== 'cls') { - setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); + if (ctx.typeMap) setIfHigherPy(ctx.typeMap, nameNode.text, typeName, 0.9); } } } @@ -377,16 +400,20 @@ function extractPythonTypeMapDepth(node, ctx, depth) { const fn = right.childForFieldName('function'); if (fn && fn.type === 'identifier') { const name = fn.text; - if (name[0] !== name[0].toLowerCase()) { - setIfHigherPy(ctx.typeMap, left.text, name, 1.0); + if (name[0] && name[0] !== name[0].toLowerCase()) { + if (ctx.typeMap) setIfHigherPy(ctx.typeMap, left.text, name, 1.0); } } if (fn && fn.type === 'attribute') { const obj = fn.childForFieldName('object'); if (obj && obj.type === 'identifier') { const objName = obj.text; - if (objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS_PY.has(objName)) { - setIfHigherPy(ctx.typeMap, left.text, objName, 0.7); + if ( + objName[0] && + objName[0] !== objName[0].toLowerCase() && + !BUILTIN_GLOBALS_PY.has(objName) + ) { + if (ctx.typeMap) setIfHigherPy(ctx.typeMap, left.text, objName, 0.7); } } } @@ -399,7 +426,7 @@ function extractPythonTypeMapDepth(node, ctx, depth) { } } -function extractPythonTypeName(typeNode) { +function extractPythonTypeName(typeNode: TreeSitterNode): string | null { if (!typeNode) return null; const t = typeNode.type; if (t === 'identifier') return typeNode.text; @@ -414,7 +441,7 @@ function extractPythonTypeName(typeNode) { return null; } -function findPythonParentClass(node) { +function findPythonParentClass(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if (current.type === 'class_definition') { diff --git a/src/extractors/ruby.js b/src/extractors/ruby.ts similarity index 80% rename from src/extractors/ruby.js rename to src/extractors/ruby.ts index cc0da5fd..6b7ba20a 100644 --- a/src/extractors/ruby.js +++ b/src/extractors/ruby.ts @@ -1,22 +1,30 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { findChild, nodeEndLine } from './helpers.js'; /** * Extract symbols from Ruby files. */ -export function extractRubySymbols(tree, _filePath) { - const ctx = { +export function extractRubySymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], classes: [], exports: [], - }; + typeMap: new Map(), + } as ExtractorOutput; walkRubyNode(tree.rootNode, ctx); return ctx; } -function walkRubyNode(node, ctx) { +function walkRubyNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'class': handleRubyClass(node, ctx); @@ -38,12 +46,15 @@ function walkRubyNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkRubyNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkRubyNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleRubyClass(node, ctx) { +function handleRubyClass(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const classChildren = extractRubyClassChildren(node); @@ -83,7 +94,7 @@ function handleRubyClass(node, ctx) { } } -function handleRubyModule(node, ctx) { +function handleRubyModule(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const moduleChildren = extractRubyBodyConstants(node); @@ -96,7 +107,7 @@ function handleRubyModule(node, ctx) { }); } -function handleRubyMethod(node, ctx) { +function handleRubyMethod(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentClass = findRubyParentClass(node); @@ -111,7 +122,7 @@ function handleRubyMethod(node, ctx) { }); } -function handleRubySingletonMethod(node, ctx) { +function handleRubySingletonMethod(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentClass = findRubyParentClass(node); @@ -126,7 +137,7 @@ function handleRubySingletonMethod(node, ctx) { }); } -function handleRubyAssignment(node, ctx) { +function handleRubyAssignment(node: TreeSitterNode, ctx: ExtractorOutput): void { if (node.parent && node.parent.type === 'program') { const left = node.childForFieldName('left'); if (left && left.type === 'constant') { @@ -140,7 +151,7 @@ function handleRubyAssignment(node, ctx) { } } -function handleRubyCall(node, ctx) { +function handleRubyCall(node: TreeSitterNode, ctx: ExtractorOutput): void { const methodNode = node.childForFieldName('method'); if (!methodNode) return; if (methodNode.text === 'require' || methodNode.text === 'require_relative') { @@ -153,13 +164,13 @@ function handleRubyCall(node, ctx) { handleRubyModuleInclusion(node, methodNode, ctx); } else { const recv = node.childForFieldName('receiver'); - const call = { name: methodNode.text, line: node.startPosition.row + 1 }; + const call: Call = { name: methodNode.text, line: node.startPosition.row + 1 }; if (recv) call.receiver = recv.text; ctx.calls.push(call); } } -function handleRubyRequire(node, ctx) { +function handleRubyRequire(node: TreeSitterNode, ctx: ExtractorOutput): void { const args = node.childForFieldName('arguments'); if (!args) return; for (let i = 0; i < args.childCount; i++) { @@ -168,7 +179,7 @@ function handleRubyRequire(node, ctx) { const strContent = arg.text.replace(/^['"]|['"]$/g, ''); ctx.imports.push({ source: strContent, - names: [strContent.split('/').pop()], + names: [strContent.split('/').pop() ?? strContent], line: node.startPosition.row + 1, rubyRequire: true, }); @@ -179,7 +190,7 @@ function handleRubyRequire(node, ctx) { if (content) { ctx.imports.push({ source: content.text, - names: [content.text.split('/').pop()], + names: [content.text.split('/').pop() ?? content.text], line: node.startPosition.row + 1, rubyRequire: true, }); @@ -189,7 +200,11 @@ function handleRubyRequire(node, ctx) { } } -function handleRubyModuleInclusion(node, _methodNode, ctx) { +function handleRubyModuleInclusion( + node: TreeSitterNode, + _methodNode: TreeSitterNode, + ctx: ExtractorOutput, +): void { const parentClass = findRubyParentClass(node); if (!parentClass) return; const args = node.childForFieldName('arguments'); @@ -206,7 +221,7 @@ function handleRubyModuleInclusion(node, _methodNode, ctx) { } } -function findRubyParentClass(node) { +function findRubyParentClass(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if (current.type === 'class' || current.type === 'module') { @@ -220,7 +235,7 @@ function findRubyParentClass(node) { // ── Child extraction helpers ──────────────────────────────────────────────── -const RUBY_PARAM_TYPES = new Set([ +const RUBY_PARAM_TYPES: Set = new Set([ 'identifier', 'optional_parameter', 'splat_parameter', @@ -229,15 +244,15 @@ const RUBY_PARAM_TYPES = new Set([ 'keyword_parameter', ]); -function extractRubyParameters(methodNode) { - const params = []; +function extractRubyParameters(methodNode: TreeSitterNode): SubDeclaration[] { + const params: SubDeclaration[] = []; const paramList = methodNode.childForFieldName('parameters') || findChild(methodNode, 'method_parameters'); if (!paramList) return params; for (let i = 0; i < paramList.childCount; i++) { const param = paramList.child(i); if (!param || !RUBY_PARAM_TYPES.has(param.type)) continue; - let name; + let name: string; if (param.type === 'identifier') { name = param.text; } else { @@ -250,8 +265,8 @@ function extractRubyParameters(methodNode) { return params; } -function extractRubyBodyConstants(containerNode) { - const children = []; +function extractRubyBodyConstants(containerNode: TreeSitterNode): SubDeclaration[] { + const children: SubDeclaration[] = []; const body = containerNode.childForFieldName('body') || findChild(containerNode, 'body'); if (!body) return children; for (let i = 0; i < body.childCount; i++) { @@ -265,8 +280,8 @@ function extractRubyBodyConstants(containerNode) { return children; } -function extractRubyClassChildren(classNode) { - const children = []; +function extractRubyClassChildren(classNode: TreeSitterNode): SubDeclaration[] { + const children: SubDeclaration[] = []; const body = classNode.childForFieldName('body') || findChild(classNode, 'body'); if (!body) return children; for (let i = 0; i < body.childCount; i++) { diff --git a/src/extractors/rust.js b/src/extractors/rust.ts similarity index 80% rename from src/extractors/rust.js rename to src/extractors/rust.ts index e601e8bc..b7e15764 100644 --- a/src/extractors/rust.js +++ b/src/extractors/rust.ts @@ -1,10 +1,17 @@ +import type { + Call, + ExtractorOutput, + SubDeclaration, + TreeSitterNode, + TreeSitterTree, +} from '../types.js'; import { findChild, nodeEndLine, rustVisibility } from './helpers.js'; /** * Extract symbols from Rust files. */ -export function extractRustSymbols(tree, _filePath) { - const ctx = { +export function extractRustSymbols(tree: TreeSitterTree, _filePath: string): ExtractorOutput { + const ctx: ExtractorOutput = { definitions: [], calls: [], imports: [], @@ -18,7 +25,7 @@ export function extractRustSymbols(tree, _filePath) { return ctx; } -function walkRustNode(node, ctx) { +function walkRustNode(node: TreeSitterNode, ctx: ExtractorOutput): void { switch (node.type) { case 'function_item': handleRustFuncItem(node, ctx); @@ -49,12 +56,15 @@ function walkRustNode(node, ctx) { break; } - for (let i = 0; i < node.childCount; i++) walkRustNode(node.child(i), ctx); + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child) walkRustNode(child, ctx); + } } // ── Walk-path per-node-type handlers ──────────────────────────────────────── -function handleRustFuncItem(node, ctx) { +function handleRustFuncItem(node: TreeSitterNode, ctx: ExtractorOutput): void { // Skip default-impl functions already emitted by handleRustTraitItem if (node.parent?.parent?.type === 'trait_item') return; const nameNode = node.childForFieldName('name'); @@ -73,7 +83,7 @@ function handleRustFuncItem(node, ctx) { }); } -function handleRustStructItem(node, ctx) { +function handleRustStructItem(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const fields = extractStructFields(node); @@ -87,7 +97,7 @@ function handleRustStructItem(node, ctx) { }); } -function handleRustEnumItem(node, ctx) { +function handleRustEnumItem(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; const variants = extractEnumVariants(node); @@ -100,7 +110,7 @@ function handleRustEnumItem(node, ctx) { }); } -function handleRustConstItem(node, ctx) { +function handleRustConstItem(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -111,7 +121,7 @@ function handleRustConstItem(node, ctx) { }); } -function handleRustTraitItem(node, ctx) { +function handleRustTraitItem(node: TreeSitterNode, ctx: ExtractorOutput): void { const nameNode = node.childForFieldName('name'); if (!nameNode) return; ctx.definitions.push({ @@ -139,7 +149,7 @@ function handleRustTraitItem(node, ctx) { } } -function handleRustImplItem(node, ctx) { +function handleRustImplItem(node: TreeSitterNode, ctx: ExtractorOutput): void { const typeNode = node.childForFieldName('type'); const traitNode = node.childForFieldName('trait'); if (typeNode && traitNode) { @@ -151,7 +161,7 @@ function handleRustImplItem(node, ctx) { } } -function handleRustUseDecl(node, ctx) { +function handleRustUseDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const argNode = node.child(1); if (!argNode) return; const usePaths = extractRustUsePath(argNode); @@ -165,7 +175,7 @@ function handleRustUseDecl(node, ctx) { } } -function handleRustCallExpr(node, ctx) { +function handleRustCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void { const fn = node.childForFieldName('function'); if (!fn) return; if (fn.type === 'identifier') { @@ -174,7 +184,7 @@ function handleRustCallExpr(node, ctx) { const field = fn.childForFieldName('field'); if (field) { const value = fn.childForFieldName('value'); - const call = { name: field.text, line: node.startPosition.row + 1 }; + const call: Call = { name: field.text, line: node.startPosition.row + 1 }; if (value) call.receiver = value.text; ctx.calls.push(call); } @@ -182,21 +192,21 @@ function handleRustCallExpr(node, ctx) { const name = fn.childForFieldName('name'); if (name) { const path = fn.childForFieldName('path'); - const call = { name: name.text, line: node.startPosition.row + 1 }; + const call: Call = { name: name.text, line: node.startPosition.row + 1 }; if (path) call.receiver = path.text; ctx.calls.push(call); } } } -function handleRustMacroInvocation(node, ctx) { +function handleRustMacroInvocation(node: TreeSitterNode, ctx: ExtractorOutput): void { const macroNode = node.child(0); if (macroNode) { ctx.calls.push({ name: `${macroNode.text}!`, line: node.startPosition.row + 1 }); } } -function findCurrentImpl(node) { +function findCurrentImpl(node: TreeSitterNode): string | null { let current = node.parent; while (current) { if (current.type === 'impl_item') { @@ -210,8 +220,8 @@ function findCurrentImpl(node) { // ── Child extraction helpers ──────────────────────────────────────────────── -function extractRustParameters(paramListNode) { - const params = []; +function extractRustParameters(paramListNode: TreeSitterNode | null): SubDeclaration[] { + const params: SubDeclaration[] = []; if (!paramListNode) return params; for (let i = 0; i < paramListNode.childCount; i++) { const param = paramListNode.child(i); @@ -228,8 +238,8 @@ function extractRustParameters(paramListNode) { return params; } -function extractStructFields(structNode) { - const fields = []; +function extractStructFields(structNode: TreeSitterNode): SubDeclaration[] { + const fields: SubDeclaration[] = []; const fieldList = structNode.childForFieldName('body') || findChild(structNode, 'field_declaration_list'); if (!fieldList) return fields; @@ -244,8 +254,8 @@ function extractStructFields(structNode) { return fields; } -function extractEnumVariants(enumNode) { - const variants = []; +function extractEnumVariants(enumNode: TreeSitterNode): SubDeclaration[] { + const variants: SubDeclaration[] = []; const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_variant_list'); if (!body) return variants; for (let i = 0; i < body.childCount; i++) { @@ -259,11 +269,11 @@ function extractEnumVariants(enumNode) { return variants; } -function extractRustTypeMap(node, ctx) { +function extractRustTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void { extractRustTypeMapDepth(node, ctx, 0); } -function extractRustTypeMapDepth(node, ctx, depth) { +function extractRustTypeMapDepth(node: TreeSitterNode, ctx: ExtractorOutput, depth: number): void { if (depth >= 200) return; // let x: MyType = ... @@ -272,7 +282,7 @@ function extractRustTypeMapDepth(node, ctx, depth) { const typeNode = node.childForFieldName('type'); if (pattern && pattern.type === 'identifier' && typeNode) { const typeName = extractRustTypeName(typeNode); - if (typeName) ctx.typeMap.set(pattern.text, { type: typeName, confidence: 0.9 }); + if (typeName) ctx.typeMap?.set(pattern.text, { type: typeName, confidence: 0.9 }); } } @@ -284,7 +294,7 @@ function extractRustTypeMapDepth(node, ctx, depth) { const name = pattern.type === 'identifier' ? pattern.text : null; if (name && name !== 'self' && name !== '&self' && name !== '&mut self') { const typeName = extractRustTypeName(typeNode); - if (typeName) ctx.typeMap.set(name, { type: typeName, confidence: 0.9 }); + if (typeName) ctx.typeMap?.set(name, { type: typeName, confidence: 0.9 }); } } } @@ -295,7 +305,7 @@ function extractRustTypeMapDepth(node, ctx, depth) { } } -function extractRustTypeName(typeNode) { +function extractRustTypeName(typeNode: TreeSitterNode): string | null { if (!typeNode) return null; const t = typeNode.type; if (t === 'type_identifier' || t === 'identifier') return typeNode.text; @@ -317,11 +327,11 @@ function extractRustTypeName(typeNode) { return null; } -function extractRustUsePath(node) { +function extractRustUsePath(node: TreeSitterNode | null): { source: string; names: string[] }[] { if (!node) return []; if (node.type === 'use_list') { - const results = []; + const results: { source: string; names: string[] }[] = []; for (let i = 0; i < node.childCount; i++) { results.push(...extractRustUsePath(node.child(i))); } @@ -333,7 +343,7 @@ function extractRustUsePath(node) { const listNode = node.childForFieldName('list'); const prefix = pathNode ? pathNode.text : ''; if (listNode) { - const names = []; + const names: string[] = []; for (let i = 0; i < listNode.childCount; i++) { const child = listNode.child(i); if ( @@ -364,7 +374,7 @@ function extractRustUsePath(node) { if (node.type === 'scoped_identifier' || node.type === 'identifier') { const text = node.text; - const lastName = text.split('::').pop(); + const lastName = text.split('::').pop() ?? text; return [{ source: text, names: [lastName] }]; } diff --git a/src/types.ts b/src/types.ts index cdc5be0c..37841b6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -487,6 +487,8 @@ export interface TreeSitterNode { namedChild(index: number): TreeSitterNode | null; childForFieldName(name: string): TreeSitterNode | null; parent: TreeSitterNode | null; + previousSibling: TreeSitterNode | null; + nextSibling: TreeSitterNode | null; children: TreeSitterNode[]; namedChildren: TreeSitterNode[]; } From 26cc36c97ec928e95a7f3d76bd9196f733681da2 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:49:48 -0600 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20add=20.js=20=E2=86=92=20.ts=20fall?= =?UTF-8?q?back=20to=20dynamic=20import=20verifier=20(#554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verify-imports script checks exact .js paths but doesn't account for the resolver hook fallback to .ts during incremental migration. Impact: 1 functions changed, 0 affected --- scripts/verify-imports.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/verify-imports.js b/scripts/verify-imports.js index bfda6f35..c97924d6 100644 --- a/scripts/verify-imports.js +++ b/scripts/verify-imports.js @@ -111,6 +111,12 @@ function resolveSpecifier(specifier, fromFile) { // Exact file exists if (existsSync(target) && statSync(target).isFile()) return null; + // .js → .ts fallback (mirrors the ESM resolver hook for incremental TS migration) + if (specifier.endsWith('.js')) { + const tsTarget = target.replace(/\.js$/, '.ts'); + if (existsSync(tsTarget) && statSync(tsTarget).isFile()) return null; + } + // Try implicit extensions (.js, .ts, .mjs, .cjs) for (const ext of ['.js', '.ts', '.mjs', '.cjs']) { if (!extname(target) && existsSync(target + ext)) return null; From 37ac14fee6349a659af1732c77acddd8bcd1ea65 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:50:00 -0600 Subject: [PATCH 05/15] fix: add load hook, remove unused imports, fix Node 20 compat (#554) - Remove unused imports (existsSync, pathToFileURL) from ts-resolve-hooks.js - Add load hook for .ts files with fallback for Node < 22.6 - Make --experimental-strip-types conditional on Node >= 22.6 in vitest config Impact: 1 functions changed, 6 affected --- scripts/ts-resolve-hooks.js | 25 +++++++++++++++++++++---- vitest.config.js | 7 ++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scripts/ts-resolve-hooks.js b/scripts/ts-resolve-hooks.js index 8da894c5..838c69aa 100644 --- a/scripts/ts-resolve-hooks.js +++ b/scripts/ts-resolve-hooks.js @@ -3,13 +3,13 @@ * * - resolve: when a .js specifier resolves to a path that doesn't exist, * check if a .ts version exists and redirect to it. - * - load: for .ts files, strip type annotations using Node 22's built-in - * --experimental-strip-types via a source transform. + * - load: for .ts files, delegate to Node's native loader (works on + * Node >= 22.6 with --experimental-strip-types); fall back to reading + * the file as module source for older versions. */ -import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; export async function resolve(specifier, context, nextResolve) { try { @@ -27,3 +27,20 @@ export async function resolve(specifier, context, nextResolve) { throw err; } } + +export async function load(url, context, nextLoad) { + if (!url.endsWith('.ts')) return nextLoad(url, context); + + // On Node >= 22.6 with --experimental-strip-types, Node handles .ts natively + try { + return await nextLoad(url, context); + } catch (err) { + if (err.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw err; + } + + // Fallback: read the file and return as module source. + // TypeScript-only syntax will cause a parse error — callers should ensure + // .ts files contain only erasable type annotations on Node < 22.6. + const source = await readFile(fileURLToPath(url), 'utf-8'); + return { format: 'module', source, shortCircuit: true }; +} diff --git a/vitest.config.js b/vitest.config.js index 513f0ab5..f304de25 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,6 +5,8 @@ import { defineConfig } from 'vitest/config'; const __dirname = dirname(fileURLToPath(import.meta.url)); const loaderPath = pathToFileURL(resolve(__dirname, 'scripts/ts-resolve-loader.js')).href; +const [major, minor] = process.versions.node.split('.').map(Number); +const supportsStripTypes = major > 22 || (major === 22 && minor >= 6); /** * During the JS → TS migration, some .js files import from modules that have @@ -42,7 +44,10 @@ export default defineConfig({ // Register the .js→.ts resolve loader for Node's native ESM resolver. // This covers require() calls and child processes spawned by tests. env: { - NODE_OPTIONS: `--experimental-strip-types --import ${loaderPath}`, + NODE_OPTIONS: [ + supportsStripTypes ? '--experimental-strip-types' : '', + `--import ${loaderPath}`, + ].filter(Boolean).join(' '), }, }, }); From 2ab13ab1b40b66ac4b3fd1275a882f508b106bcf Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:25:05 -0600 Subject: [PATCH 06/15] fix: add load hook, preserve NODE_OPTIONS, fix Node 20 compat (#554) - ts-resolve-hooks.js load hook throws ERR_TS_UNSUPPORTED on Node < 22.6 - vitest.config.js preserves existing NODE_OPTIONS and uses --strip-types on Node >= 23 - skip child-process tests on Node < 22.6 (cli, batch CLI, CJS wrapper) --- scripts/ts-resolve-hooks.js | 20 ++++++++++++-------- tests/integration/batch.test.js | 6 +++++- tests/integration/cli.test.js | 8 ++++++-- tests/unit/index-exports.test.js | 6 +++++- vitest.config.js | 3 ++- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/scripts/ts-resolve-hooks.js b/scripts/ts-resolve-hooks.js index 838c69aa..4423a33e 100644 --- a/scripts/ts-resolve-hooks.js +++ b/scripts/ts-resolve-hooks.js @@ -4,11 +4,10 @@ * - resolve: when a .js specifier resolves to a path that doesn't exist, * check if a .ts version exists and redirect to it. * - load: for .ts files, delegate to Node's native loader (works on - * Node >= 22.6 with --experimental-strip-types); fall back to reading - * the file as module source for older versions. + * Node >= 22.6 with --experimental-strip-types). On older Node versions, + * throws a clear error instead of returning unparseable TypeScript source. */ -import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; export async function resolve(specifier, context, nextResolve) { @@ -38,9 +37,14 @@ export async function load(url, context, nextLoad) { if (err.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw err; } - // Fallback: read the file and return as module source. - // TypeScript-only syntax will cause a parse error — callers should ensure - // .ts files contain only erasable type annotations on Node < 22.6. - const source = await readFile(fileURLToPath(url), 'utf-8'); - return { format: 'module', source, shortCircuit: true }; + // Node < 22.6 cannot strip TypeScript syntax. Throw a clear error instead + // of returning raw TS source that would produce a confusing SyntaxError. + const filePath = fileURLToPath(url); + throw Object.assign( + new Error( + `Cannot load TypeScript file ${filePath} on Node ${process.versions.node}. ` + + `TypeScript type stripping requires Node >= 22.6 with --experimental-strip-types.`, + ), + { code: 'ERR_TS_UNSUPPORTED' }, + ); } diff --git a/tests/integration/batch.test.js b/tests/integration/batch.test.js index e903645c..216c98e3 100644 --- a/tests/integration/batch.test.js +++ b/tests/integration/batch.test.js @@ -25,6 +25,10 @@ import { splitTargets, } from '../../src/features/batch.js'; +// Child processes load .ts files natively — requires Node >= 22.6 type stripping +const [_major, _minor] = process.versions.node.split('.').map(Number); +const canStripTypes = _major > 22 || (_major === 22 && _minor >= 6); + // ─── Helpers ─────────────────────────────────────────────────────────── function insertNode(db, name, kind, file, line) { @@ -208,7 +212,7 @@ describe('batchData — complexity (dbOnly signature)', () => { // ─── CLI smoke test ────────────────────────────────────────────────── -describe('batch CLI', () => { +describe.skipIf(!canStripTypes)('batch CLI', () => { const cliPath = path.resolve( path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1')), '../../src/cli.js', diff --git a/tests/integration/cli.test.js b/tests/integration/cli.test.js index a366cd8c..4738f645 100644 --- a/tests/integration/cli.test.js +++ b/tests/integration/cli.test.js @@ -9,6 +9,10 @@ import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +// Child processes load .ts files natively — requires Node >= 22.6 type stripping +const [_major, _minor] = process.versions.node.split('.').map(Number); +const canStripTypes = _major > 22 || (_major === 22 && _minor >= 6); + const CLI = path.resolve('src/cli.js'); const FIXTURE_FILES = { @@ -65,7 +69,7 @@ afterAll(() => { if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true }); }); -describe('CLI smoke tests', () => { +describe.skipIf(!canStripTypes)('CLI smoke tests', () => { // ─── Build ─────────────────────────────────────────────────────────── test('build creates graph.db', () => { expect(fs.existsSync(dbPath)).toBe(true); @@ -237,7 +241,7 @@ describe('CLI smoke tests', () => { // ─── Registry CLI ─────────────────────────────────────────────────────── -describe('Registry CLI commands', () => { +describe.skipIf(!canStripTypes)('Registry CLI commands', () => { let tmpHome; /** Run CLI with isolated HOME to avoid touching real registry */ diff --git a/tests/unit/index-exports.test.js b/tests/unit/index-exports.test.js index 919af2e2..a550177d 100644 --- a/tests/unit/index-exports.test.js +++ b/tests/unit/index-exports.test.js @@ -4,6 +4,10 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +// Child processes load .ts files natively — requires Node >= 22.6 type stripping +const [_major, _minor] = process.versions.node.split('.').map(Number); +const canStripTypes = _major > 22 || (_major === 22 && _minor >= 6); + const __dirname = dirname(fileURLToPath(import.meta.url)); const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')); @@ -22,7 +26,7 @@ describe('index.js re-exports', () => { expect(typeof mod).toBe('object'); }); - it('CJS wrapper resolves to the same exports', async () => { + it.skipIf(!canStripTypes)('CJS wrapper resolves to the same exports', async () => { const require = createRequire(import.meta.url); const cjs = await require('../../src/index.cjs'); const esm = await import('../../src/index.js'); diff --git a/vitest.config.js b/vitest.config.js index f304de25..73b8b865 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -45,7 +45,8 @@ export default defineConfig({ // This covers require() calls and child processes spawned by tests. env: { NODE_OPTIONS: [ - supportsStripTypes ? '--experimental-strip-types' : '', + process.env.NODE_OPTIONS, + supportsStripTypes ? (major >= 23 ? '--strip-types' : '--experimental-strip-types') : '', `--import ${loaderPath}`, ].filter(Boolean).join(' '), }, From 447510aacdcb0f082e177c9a3f34c1253c638393 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:31:19 -0600 Subject: [PATCH 07/15] fix: replace CJS require() with ESM import in formatCycles tests (#554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit require() bypasses Vite's .js→.ts resolver plugin, causing ERR_MODULE_NOT_FOUND for renamed .ts files on all Node versions. Use the top-level ESM import instead. --- tests/graph/cycles.test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/graph/cycles.test.js b/tests/graph/cycles.test.js index 2dd8a2ea..18014f8c 100644 --- a/tests/graph/cycles.test.js +++ b/tests/graph/cycles.test.js @@ -5,7 +5,7 @@ import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { initSchema } from '../../src/db/index.js'; -import { findCycles, findCyclesJS } from '../../src/domain/graph/cycles.js'; +import { findCycles, findCyclesJS, formatCycles } from '../../src/domain/graph/cycles.js'; import { isNativeAvailable, loadNative } from '../../src/infrastructure/native.js'; const hasNative = isNativeAvailable(); @@ -123,13 +123,11 @@ describe('findCycles — function-level', () => { describe('formatCycles', () => { it('returns no-cycles message for empty array', () => { - const { formatCycles } = require('../../src/domain/graph/cycles.js'); const output = formatCycles([]); expect(output.toLowerCase()).toMatch(/no.*circular/); }); it('formats a single cycle with all member files', () => { - const { formatCycles } = require('../../src/domain/graph/cycles.js'); const output = formatCycles([['a.js', 'b.js']]); expect(output).toContain('a.js'); expect(output).toContain('b.js'); @@ -137,7 +135,6 @@ describe('formatCycles', () => { }); it('formats multiple cycles with distinct labels', () => { - const { formatCycles } = require('../../src/domain/graph/cycles.js'); const output = formatCycles([ ['a.js', 'b.js'], ['x.js', 'y.js', 'z.js'], From 8ae7c6d4c09903ff1d68fd31007524c675b9a787 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:53:29 -0600 Subject: [PATCH 08/15] fix: add dedup guards, Node 20.6 guard, simplify type assertion (#554) - vitest.config.js: dedup --strip-types and --import flags - ts-resolve-loader.js: guard module.register() for Node >= 20.6 - in-memory-repository.ts: simplify complex type assertion to `as string` - Fix TS errors from main merge (models.ts, helpers.ts, pipeline.ts, watcher.ts) --- scripts/ts-resolve-loader.js | 11 +++++++---- src/db/repository/in-memory-repository.ts | 4 +--- vitest.config.js | 11 ++++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/ts-resolve-loader.js b/scripts/ts-resolve-loader.js index aa7f4531..4ef5ca8a 100644 --- a/scripts/ts-resolve-loader.js +++ b/scripts/ts-resolve-loader.js @@ -9,7 +9,10 @@ * (or via NODE_OPTIONS / vitest poolOptions.execArgv) */ -import { register } from 'node:module'; - -const hooksURL = new URL('./ts-resolve-hooks.js', import.meta.url); -register(hooksURL.href, { parentURL: import.meta.url }); +// module.register() requires Node >= 20.6.0 +const [_major, _minor] = process.versions.node.split('.').map(Number); +if (_major > 20 || (_major === 20 && _minor >= 6)) { + const { register } = await import('node:module'); + const hooksURL = new URL('./ts-resolve-hooks.js', import.meta.url); + register(hooksURL.href, { parentURL: import.meta.url }); +} diff --git a/src/db/repository/in-memory-repository.ts b/src/db/repository/in-memory-repository.ts index f2667e35..c3bc2e36 100644 --- a/src/db/repository/in-memory-repository.ts +++ b/src/db/repository/in-memory-repository.ts @@ -178,9 +178,7 @@ export class InMemoryRepository extends Repository { let nodes = [...this.#nodes.values()].filter((n) => re.test(n.name)); if (opts.kinds) { - nodes = nodes.filter((n) => - opts.kinds?.includes(n.kind as typeof opts.kinds extends (infer U)[] ? U : never), - ); + nodes = nodes.filter((n) => opts.kinds?.includes(n.kind as string)); } { const fileFn = buildFileFilterFn(opts.file); diff --git a/vitest.config.js b/vitest.config.js index 73b8b865..d7ac9a1c 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,6 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const loaderPath = pathToFileURL(resolve(__dirname, 'scripts/ts-resolve-loader.js')).href; const [major, minor] = process.versions.node.split('.').map(Number); const supportsStripTypes = major > 22 || (major === 22 && minor >= 6); +const existing = process.env.NODE_OPTIONS || ''; /** * During the JS → TS migration, some .js files import from modules that have @@ -45,9 +46,13 @@ export default defineConfig({ // This covers require() calls and child processes spawned by tests. env: { NODE_OPTIONS: [ - process.env.NODE_OPTIONS, - supportsStripTypes ? (major >= 23 ? '--strip-types' : '--experimental-strip-types') : '', - `--import ${loaderPath}`, + existing, + supportsStripTypes && + !existing.includes('--experimental-strip-types') && + !existing.includes('--strip-types') + ? (major >= 23 ? '--strip-types' : '--experimental-strip-types') + : '', + existing.includes(loaderPath) ? '' : `--import ${loaderPath}`, ].filter(Boolean).join(' '), }, }, From d1dc03bc90cc03ac06db1bff6cbdf64ba72d57e0 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:58:41 -0600 Subject: [PATCH 09/15] fix: cast kind to SymbolKind instead of string in includes() (#554) The `as string` cast widened the type beyond what `includes()` accepts on `SymbolKind[]`. Cast to `SymbolKind` instead. --- src/db/repository/in-memory-repository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/db/repository/in-memory-repository.ts b/src/db/repository/in-memory-repository.ts index c3bc2e36..42576f5f 100644 --- a/src/db/repository/in-memory-repository.ts +++ b/src/db/repository/in-memory-repository.ts @@ -178,7 +178,9 @@ export class InMemoryRepository extends Repository { let nodes = [...this.#nodes.values()].filter((n) => re.test(n.name)); if (opts.kinds) { - nodes = nodes.filter((n) => opts.kinds?.includes(n.kind as string)); + nodes = nodes.filter((n) => + opts.kinds?.includes(n.kind as import('../../types.js').SymbolKind), + ); } { const fileFn = buildFileFilterFn(opts.file); From 897993bcfc189471ea223a7dd7793033f235e89e Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:11:33 -0600 Subject: [PATCH 10/15] fix: resolve readonly array includes TS errors from main merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EVERY_SYMBOL_KIND and CORE_SYMBOL_KINDS are readonly-typed after kinds.js was converted to kinds.ts in #553. Array.prototype.includes on a readonly T[] rejects a wider argument type — cast to readonly string[] at call sites where the argument is string/AnyNodeKind. Also spread CORE_SYMBOL_KINDS where a mutable string[] is expected. --- src/db/repository/in-memory-repository.ts | 4 ++-- src/db/repository/nodes.ts | 2 +- src/domain/analysis/implementations.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/db/repository/in-memory-repository.ts b/src/db/repository/in-memory-repository.ts index 42576f5f..c1174f11 100644 --- a/src/db/repository/in-memory-repository.ts +++ b/src/db/repository/in-memory-repository.ts @@ -286,7 +286,7 @@ export class InMemoryRepository extends Repository { } findNodesForTriage(opts: TriageQueryOpts = {}): NodeRow[] { - if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) { + if (opts.kind && !(EVERY_SYMBOL_KIND as readonly string[]).includes(opts.kind)) { throw new ConfigError( `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`, ); @@ -563,7 +563,7 @@ export class InMemoryRepository extends Repository { getCallableNodes(): CallableNodeRow[] { return [...this.#nodes.values()] - .filter((n) => CORE_SYMBOL_KINDS.includes(n.kind)) + .filter((n) => (CORE_SYMBOL_KINDS as readonly string[]).includes(n.kind)) .map((n) => ({ id: n.id, name: n.name, kind: n.kind, file: n.file })); } diff --git a/src/db/repository/nodes.ts b/src/db/repository/nodes.ts index 1b0a58ff..2fcb3ded 100644 --- a/src/db/repository/nodes.ts +++ b/src/db/repository/nodes.ts @@ -46,7 +46,7 @@ export function findNodesForTriage( db: BetterSqlite3Database, opts: TriageQueryOpts = {}, ): NodeRow[] { - if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) { + if (opts.kind && !(EVERY_SYMBOL_KIND as readonly string[]).includes(opts.kind)) { throw new ConfigError( `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`, ); diff --git a/src/domain/analysis/implementations.ts b/src/domain/analysis/implementations.ts index 3606a285..7281e6ac 100644 --- a/src/domain/analysis/implementations.ts +++ b/src/domain/analysis/implementations.ts @@ -27,7 +27,7 @@ export function implementationsData( noTests, file: opts.file, kind: opts.kind, - kinds: opts.kind ? undefined : CORE_SYMBOL_KINDS, + kinds: opts.kind ? undefined : [...CORE_SYMBOL_KINDS], }); if (nodes.length === 0) { return { name, results: [] }; @@ -78,7 +78,7 @@ export function interfacesData( noTests, file: opts.file, kind: opts.kind, - kinds: opts.kind ? undefined : CORE_SYMBOL_KINDS, + kinds: opts.kind ? undefined : [...CORE_SYMBOL_KINDS], }); if (nodes.length === 0) { return { name, results: [] }; From 306b833367571d1c44a0e2175e2d0fcea4a77458 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:18:52 -0600 Subject: [PATCH 11/15] fix: guard --import flag for Node >= 20.6 and add implements IRepository to base class - vitest.config.js: only inject --import into NODE_OPTIONS when Node >= 20.6, matching the same boundary enforced inside ts-resolve-loader.js - src/db/repository/base.ts: add implements IRepository so TypeScript statically enforces that the class stays in sync with the interface in types.ts --- src/db/repository/base.ts | 3 ++- vitest.config.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/db/repository/base.ts b/src/db/repository/base.ts index 2a78902c..1fd44bba 100644 --- a/src/db/repository/base.ts +++ b/src/db/repository/base.ts @@ -8,6 +8,7 @@ import type { ImportEdgeRow, ImportGraphEdgeRow, IntraFileCallEdge, + Repository as IRepository, ListFunctionOpts, NodeIdRow, NodeRow, @@ -23,7 +24,7 @@ import type { * Defines the contract for all graph data access. Every method throws * "not implemented" by default — concrete subclasses override what they support. */ -export class Repository { +export class Repository implements IRepository { // ── Node lookups ──────────────────────────────────────────────────── findNodeById(_id: number): NodeRow | undefined { throw new Error('not implemented'); diff --git a/vitest.config.js b/vitest.config.js index d7ac9a1c..04d915bf 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,6 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const loaderPath = pathToFileURL(resolve(__dirname, 'scripts/ts-resolve-loader.js')).href; const [major, minor] = process.versions.node.split('.').map(Number); const supportsStripTypes = major > 22 || (major === 22 && minor >= 6); +const supportsHooks = major > 20 || (major === 20 && minor >= 6); const existing = process.env.NODE_OPTIONS || ''; /** @@ -52,7 +53,7 @@ export default defineConfig({ !existing.includes('--strip-types') ? (major >= 23 ? '--strip-types' : '--experimental-strip-types') : '', - existing.includes(loaderPath) ? '' : `--import ${loaderPath}`, + existing.includes(loaderPath) ? '' : (supportsHooks ? `--import ${loaderPath}` : ''), ].filter(Boolean).join(' '), }, }, From 7465f9bfc961605fe5e824a420bced580bf7807c Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:38:59 -0600 Subject: [PATCH 12/15] fix: correct NativeAddon.resolveImports signature and remove unused disposeParsers parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - types.ts: fix resolveImport and resolveImports signatures to match actual native call sites in resolve.ts (remove spurious extensions param, add knownFiles param, fix return type) - domain/parser.ts: remove unused _parsers parameter from disposeParsers — no caller ever passes it, and the function always operates on module-level caches - domain/analysis/dependencies.ts: move biome-ignore to correct line (suppression had no effect on wrong line) Impact: 4 functions changed, 9 affected --- src/domain/analysis/dependencies.ts | 2 +- src/domain/parser.ts | 2 +- src/types.ts | 12 +++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/domain/analysis/dependencies.ts b/src/domain/analysis/dependencies.ts index 3f251a9f..c0131f47 100644 --- a/src/domain/analysis/dependencies.ts +++ b/src/domain/analysis/dependencies.ts @@ -74,9 +74,9 @@ function buildTransitiveCallers( const visited = new Set([nodeId]); let frontier = callers .map((c) => { - // biome-ignore lint/suspicious/noExplicitAny: DB row type const row = db .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?') + // biome-ignore lint/suspicious/noExplicitAny: DB row type .get(c.name, c.kind, c.file, c.line) as any; return row ? { ...c, id: row.id } : null; }) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index ea83066e..04ee55eb 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -115,7 +115,7 @@ export async function createParsers(): Promise> { * Call this between repeated builds in the same process (e.g. benchmarks) * to prevent memory accumulation that can cause segfaults. */ -export function disposeParsers(_parsers?: Map): void { +export function disposeParsers(): void { if (_cachedParsers) { for (const [id, parser] of _cachedParsers) { if (parser && typeof parser.delete === 'function') { diff --git a/src/types.ts b/src/types.ts index 37841b6b..8fc5e95a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1678,19 +1678,13 @@ export interface NativeAddon { dataflow: boolean, ast: boolean, ): unknown[]; - resolveImport( - fromFile: string, - importSource: string, - rootDir: string, - extensions: string[], - aliases: unknown, - ): string | null; + resolveImport(fromFile: string, importSource: string, rootDir: string, aliases: unknown): string; resolveImports( items: Array<{ fromFile: string; importSource: string }>, rootDir: string, - extensions: string[], aliases: unknown, - ): Array<{ key: string; resolved: string | null }>; + knownFiles: string[] | null, + ): Array<{ fromFile: string; importSource: string; resolvedPath: string }>; computeConfidence(callerFile: string, targetFile: string, importedFrom: string | null): number; detectCycles(edges: Array<{ source: string; target: string }>): string[][]; buildCallEdges(files: unknown[], nodes: unknown[], builtinReceivers: string[]): unknown[]; From b08ce30063de066260141cd0513d2efe14ac20f0 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:24:35 -0600 Subject: [PATCH 13/15] fix: correct NativeAddon.parseFiles signature and relax transaction type parseFiles: fix parameter order/types to match actual call site in parser.ts (string[], rootDir, dataflow, ast) instead of ({filePath,source}[], dataflow, ast). transaction: relax parameter types to be compatible with better-sqlite3's generic Transaction, allowing direct cast without intermediate unknown. --- src/types.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/types.ts b/src/types.ts index 8fc5e95a..9aa8bf7c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1658,7 +1658,8 @@ export interface BetterSqlite3Database { exec(sql: string): void; close(): void; pragma(sql: string): unknown; - transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T; + // biome-ignore lint/suspicious/noExplicitAny: must be compatible with better-sqlite3's generic Transaction return type + transaction(fn: (...args: any[]) => T): (...args: any[]) => T; readonly open: boolean; readonly name: string; } @@ -1673,11 +1674,7 @@ export type StmtCache = WeakMap, - dataflow: boolean, - ast: boolean, - ): unknown[]; + parseFiles(files: string[], rootDir: string, dataflow: boolean, ast: boolean): unknown[]; resolveImport(fromFile: string, importSource: string, rootDir: string, aliases: unknown): string; resolveImports( items: Array<{ fromFile: string; importSource: string }>, From 4a63243f99a130f4cf3d3815c77e2dfb52b1ec1a Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:24:45 -0600 Subject: [PATCH 14/15] fix: document double-cast in purgeFilesFromGraph Add comment explaining why as unknown as BetterSqlite3Database is needed: better-sqlite3 types don't declare open/name properties that the project interface requires. --- src/domain/graph/builder/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/graph/builder/helpers.ts b/src/domain/graph/builder/helpers.ts index 4f58cafb..05ede297 100644 --- a/src/domain/graph/builder/helpers.ts +++ b/src/domain/graph/builder/helpers.ts @@ -203,6 +203,7 @@ export function purgeFilesFromGraph( files: string[], options: Record = {}, ): void { + // Double-cast needed: better-sqlite3 types don't declare `open`/`name` properties purgeFilesData(db as unknown as BetterSqlite3Database, files, options); } From d0b4b8d3904a15c88831016fd379e1c4f348e14d Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:24:53 -0600 Subject: [PATCH 15/15] fix: restore typed db in watcher.ts instead of widening to any Replace db: any with proper BetterSqlite3.Database type annotation. Use typedDb alias for functions expecting the project's BetterSqlite3Database interface, avoiding untyped code paths. --- src/domain/graph/watcher.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/graph/watcher.ts b/src/domain/graph/watcher.ts index 7cbd2129..c7948355 100644 --- a/src/domain/graph/watcher.ts +++ b/src/domain/graph/watcher.ts @@ -24,9 +24,9 @@ export async function watchProject(rootDir: string, opts: { engine?: string } = throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath }); } - // openDb comes from untyped JS — leave as `any` since consumers expect different DB types - // biome-ignore lint/suspicious/noExplicitAny: openDb is untyped JS - const db: any = openDb(dbPath); + const db = openDb(dbPath) as import('better-sqlite3').Database; + // Alias for functions expecting the project's BetterSqlite3Database interface + const typedDb = db as unknown as import('../../types.js').BetterSqlite3Database; initSchema(db); const engineOpts = { engine: (opts.engine || 'auto') as import('../../types.js').EngineMode }; const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts); @@ -47,7 +47,7 @@ export async function watchProject(rootDir: string, opts: { engine?: string } = ), getNodeId: { get: (name: string, kind: string, file: string, line: number) => { - const id = getNodeIdQuery(db, name, kind, file, line); + const id = getNodeIdQuery(typedDb, name, kind, file, line); return id != null ? { id } : undefined; }, },