From f5ee3dee4d116fde881a072d98d111ded6f1d4fd Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:05:31 -0600 Subject: [PATCH 01/14] chore: remove dead exports and un-export internal constant - Remove dead `truncate` function from ast-analysis/shared.js (0 consumers) - Remove dead `truncStart` function from presentation/table.js (0 consumers) - Un-export `BATCH_CHUNK` in builder/helpers.js (only used internally) Skipped sync.json targets that were false positives: - BUILTIN_RECEIVERS: used by incremental.js + build-edges.js - TRANSIENT_CODES/RETRY_DELAY_MS: internal to readFileSafe - MAX_COL_WIDTH: internal to printAutoTable - findFunctionNode: re-exported from index.js, used in tests Impact: 1 functions changed, 32 affected --- src/ast-analysis/shared.js | 12 ------------ src/domain/graph/builder/helpers.js | 2 +- src/presentation/table.js | 8 -------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/ast-analysis/shared.js b/src/ast-analysis/shared.js index 964f9a06..e3f40bd0 100644 --- a/src/ast-analysis/shared.js +++ b/src/ast-analysis/shared.js @@ -176,18 +176,6 @@ export function findFunctionNode(rootNode, startLine, _endLine, rules) { return best; } -/** - * Truncate a string to a maximum length, appending an ellipsis if truncated. - * - * @param {string} str - Input string - * @param {number} [max=200] - Maximum length - * @returns {string} - */ -export function truncate(str, max = 200) { - if (!str) return ''; - return str.length > max ? `${str.slice(0, max)}…` : str; -} - // ─── Extension / Language Mapping ───────────────────────────────────────── /** diff --git a/src/domain/graph/builder/helpers.js b/src/domain/graph/builder/helpers.js index 038de4c2..b7916c84 100644 --- a/src/domain/graph/builder/helpers.js +++ b/src/domain/graph/builder/helpers.js @@ -179,7 +179,7 @@ export function purgeFilesFromGraph(db, files, options = {}) { } /** Batch INSERT chunk size for multi-value INSERTs. */ -export const BATCH_CHUNK = 200; +const BATCH_CHUNK = 200; /** * Batch-insert node rows via multi-value INSERT statements. diff --git a/src/presentation/table.js b/src/presentation/table.js index d5ef1903..4fdba379 100644 --- a/src/presentation/table.js +++ b/src/presentation/table.js @@ -37,11 +37,3 @@ export function truncEnd(str, maxLen) { if (str.length <= maxLen) return str; return `${str.slice(0, maxLen - 1)}\u2026`; } - -/** - * Truncate a string from the start, prepending '\u2026' if truncated. - */ -export function truncStart(str, maxLen) { - if (str.length <= maxLen) return str; - return `\u2026${str.slice(-(maxLen - 1))}`; -} From 17cdcb00984f582485f8582734a40e3df4211d10 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:14:13 -0600 Subject: [PATCH 02/14] refactor: extract shared findNodes utility from cfg and dataflow features Impact: 5 functions changed, 7 affected --- src/features/cfg.js | 31 +++++------------ src/features/dataflow.js | 55 +++++++++++++++---------------- src/features/shared/find-nodes.js | 32 ++++++++++++++++++ 3 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 src/features/shared/find-nodes.js diff --git a/src/features/cfg.js b/src/features/cfg.js index e8728cab..eff08652 100644 --- a/src/features/cfg.js +++ b/src/features/cfg.js @@ -24,8 +24,8 @@ import { openReadonlyOrFail, } from '../db/index.js'; import { info } from '../infrastructure/logger.js'; -import { isTestFile } from '../infrastructure/test-filter.js'; import { paginateResult } from '../shared/paginate.js'; +import { findNodes } from './shared/find-nodes.js'; // Re-export for backward compatibility export { _makeCfgRules as makeCfgRules, CFG_RULES }; @@ -273,27 +273,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) { // ─── Query-Time Functions ─────────────────────────────────────────────── -function findNodes(db, name, opts = {}) { - const kinds = opts.kind ? [opts.kind] : ['function', 'method']; - const placeholders = kinds.map(() => '?').join(', '); - const params = [`%${name}%`, ...kinds]; - - let fileCondition = ''; - if (opts.file) { - fileCondition = ' AND n.file LIKE ?'; - params.push(`%${opts.file}%`); - } - - const rows = db - .prepare( - `SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line - FROM nodes n - WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`, - ) - .all(...params); - - return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows; -} +const CFG_DEFAULT_KINDS = ['function', 'method']; /** * Load CFG data for a function from the database. @@ -317,7 +297,12 @@ export function cfgData(name, customDbPath, opts = {}) { }; } - const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }); + const nodes = findNodes( + db, + name, + { noTests, file: opts.file, kind: opts.kind }, + CFG_DEFAULT_KINDS, + ); if (nodes.length === 0) { return { name, results: [] }; } diff --git a/src/features/dataflow.js b/src/features/dataflow.js index 9d0c8bcc..0f500b8f 100644 --- a/src/features/dataflow.js +++ b/src/features/dataflow.js @@ -24,6 +24,7 @@ import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js'; import { info } from '../infrastructure/logger.js'; import { isTestFile } from '../infrastructure/test-filter.js'; import { paginateResult } from '../shared/paginate.js'; +import { findNodes } from './shared/find-nodes.js'; // Re-export for backward compatibility export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES }; @@ -234,31 +235,7 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) // ── Query functions ───────────────────────────────────────────────────────── -/** - * Look up node(s) by name with optional file/kind/noTests filtering. - * Similar to findMatchingNodes in queries.js but operates on the dataflow table. - */ -function findNodes(db, name, opts = {}) { - const kinds = opts.kind ? [opts.kind] : ALL_SYMBOL_KINDS; - const placeholders = kinds.map(() => '?').join(', '); - const params = [`%${name}%`, ...kinds]; - - let fileCondition = ''; - if (opts.file) { - fileCondition = ' AND file LIKE ?'; - params.push(`%${opts.file}%`); - } - - const rows = db - .prepare( - `SELECT * FROM nodes - WHERE name LIKE ? AND kind IN (${placeholders})${fileCondition} - ORDER BY file, line`, - ) - .all(...params); - - return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows; -} +// findNodes imported from ./shared/find-nodes.js /** * Return all dataflow edges for a symbol. @@ -282,7 +259,12 @@ export function dataflowData(name, customDbPath, opts = {}) { }; } - const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }); + const nodes = findNodes( + db, + name, + { noTests, file: opts.file, kind: opts.kind }, + ALL_SYMBOL_KINDS, + ); if (nodes.length === 0) { return { name, results: [] }; } @@ -426,12 +408,22 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) { }; } - const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind }); + const fromNodes = findNodes( + db, + from, + { noTests, file: opts.fromFile, kind: opts.kind }, + ALL_SYMBOL_KINDS, + ); if (fromNodes.length === 0) { return { from, to, found: false, error: `No symbol matching "${from}"` }; } - const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind }); + const toNodes = findNodes( + db, + to, + { noTests, file: opts.toFile, kind: opts.kind }, + ALL_SYMBOL_KINDS, + ); if (toNodes.length === 0) { return { from, to, found: false, error: `No symbol matching "${to}"` }; } @@ -554,7 +546,12 @@ export function dataflowImpactData(name, customDbPath, opts = {}) { }; } - const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }); + const nodes = findNodes( + db, + name, + { noTests, file: opts.file, kind: opts.kind }, + ALL_SYMBOL_KINDS, + ); if (nodes.length === 0) { return { name, results: [] }; } diff --git a/src/features/shared/find-nodes.js b/src/features/shared/find-nodes.js new file mode 100644 index 00000000..cc886d80 --- /dev/null +++ b/src/features/shared/find-nodes.js @@ -0,0 +1,32 @@ +import { isTestFile } from '../../infrastructure/test-filter.js'; + +/** + * Look up node(s) by name with optional file/kind/noTests filtering. + * + * @param {object} db - open SQLite database handle + * @param {string} name - symbol name (partial LIKE match) + * @param {object} [opts] - { kind, file, noTests } + * @param {string[]} defaultKinds - fallback kinds when opts.kind is not set + * @returns {object[]} matching node rows + */ +export function findNodes(db, name, opts = {}, defaultKinds) { + const kinds = opts.kind ? [opts.kind] : defaultKinds; + const placeholders = kinds.map(() => '?').join(', '); + const params = [`%${name}%`, ...kinds]; + + let fileCondition = ''; + if (opts.file) { + fileCondition = ' AND file LIKE ?'; + params.push(`%${opts.file}%`); + } + + const rows = db + .prepare( + `SELECT * FROM nodes + WHERE name LIKE ? AND kind IN (${placeholders})${fileCondition} + ORDER BY file, line`, + ) + .all(...params); + + return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows; +} From a09740d9184ea58b3cdcecfeebb964f8743594e8 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:18:59 -0600 Subject: [PATCH 03/14] fix: replace empty catch blocks in db connection and migrations connection.js: add debug() logging to all 8 catch-with-fallback blocks so failures are observable without changing behavior. migrations.js: replace 14 try/catch blocks in initSchema with hasColumn() and hasTable() guards. CREATE INDEX calls use IF NOT EXISTS directly. getBuildMeta uses hasTable() check instead of try/catch. Impact: 10 functions changed, 19 affected --- src/db/connection.js | 30 +++++++++------- src/db/migrations.js | 86 +++++++++++++++----------------------------- 2 files changed, 46 insertions(+), 70 deletions(-) diff --git a/src/db/connection.js b/src/db/connection.js index 75ee4a6d..59114bbd 100644 --- a/src/db/connection.js +++ b/src/db/connection.js @@ -37,10 +37,12 @@ export function findRepoRoot(fromDir) { // matches the realpathSync'd dir in findDbPath. try { root = fs.realpathSync(raw); - } catch { + } catch (e) { + debug(`realpathSync failed for git root "${raw}", using resolve: ${e.message}`); root = path.resolve(raw); } - } catch { + } catch (e) { + debug(`git rev-parse failed for "${dir}": ${e.message}`); root = null; } if (!fromDir) { @@ -60,7 +62,8 @@ function isProcessAlive(pid) { try { process.kill(pid, 0); return true; - } catch { + } catch (e) { + debug(`PID ${pid} not alive: ${e.code || e.message}`); return false; } } @@ -75,13 +78,13 @@ function acquireAdvisoryLock(dbPath) { warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`); } } - } catch { - /* ignore read errors */ + } catch (e) { + debug(`Advisory lock read failed: ${e.message}`); } try { fs.writeFileSync(lockPath, String(process.pid), 'utf-8'); - } catch { - /* best-effort */ + } catch (e) { + debug(`Advisory lock write failed: ${e.message}`); } } @@ -91,8 +94,8 @@ function releaseAdvisoryLock(lockPath) { if (Number(content) === process.pid) { fs.unlinkSync(lockPath); } - } catch { - /* ignore */ + } catch (e) { + debug(`Advisory lock release failed for ${lockPath}: ${e.message}`); } } @@ -107,7 +110,8 @@ function isSameDirectory(a, b) { const sa = fs.statSync(a); const sb = fs.statSync(b); return sa.dev === sb.dev && sa.ino === sb.ino; - } catch { + } catch (e) { + debug(`isSameDirectory stat failed: ${e.message}`); return false; } } @@ -139,7 +143,8 @@ export function findDbPath(customPath) { if (rawCeiling) { try { ceiling = fs.realpathSync(rawCeiling); - } catch { + } catch (e) { + debug(`realpathSync failed for ceiling "${rawCeiling}": ${e.message}`); ceiling = rawCeiling; } } else { @@ -149,7 +154,8 @@ export function findDbPath(customPath) { let dir; try { dir = fs.realpathSync(process.cwd()); - } catch { + } catch (e) { + debug(`realpathSync failed for cwd: ${e.message}`); dir = process.cwd(); } while (true) { diff --git a/src/db/migrations.js b/src/db/migrations.js index 3b38feff..8a12bda2 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -242,13 +242,20 @@ export const MIGRATIONS = [ }, ]; +function hasColumn(db, table, column) { + const cols = db.pragma(`table_info(${table})`); + return cols.some((c) => c.name === column); +} + +function hasTable(db, table) { + const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table); + return !!row; +} + export function getBuildMeta(db, key) { - try { - const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key); - return row ? row.value : null; - } catch { - return null; - } + if (!hasTable(db, 'build_meta')) return null; + const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key); + return row ? row.value : null; } export function setBuildMeta(db, entries) { @@ -280,74 +287,37 @@ export function initSchema(db) { } } - try { + // Legacy column compat — add columns that may be missing from pre-migration DBs + if (!hasColumn(db, 'nodes', 'end_line')) { db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER'); - } catch { - /* already exists */ } - try { + if (!hasColumn(db, 'edges', 'confidence')) { db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0'); - } catch { - /* already exists */ } - try { + if (!hasColumn(db, 'edges', 'dynamic')) { db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0'); - } catch { - /* already exists */ } - try { + if (!hasColumn(db, 'nodes', 'role')) { db.exec('ALTER TABLE nodes ADD COLUMN role TEXT'); - } catch { - /* already exists */ } - try { - db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)'); - } catch { - /* already exists */ - } - try { + db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)'); + if (!hasColumn(db, 'nodes', 'parent_id')) { db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)'); - } catch { - /* already exists */ - } - try { - db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)'); - } catch { - /* already exists */ - } - try { - db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)'); - } catch { - /* already exists */ } - try { + db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)'); + if (!hasColumn(db, 'nodes', 'qualified_name')) { db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT'); - } catch { - /* already exists */ } - try { + if (!hasColumn(db, 'nodes', 'scope')) { db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT'); - } catch { - /* already exists */ } - try { + if (!hasColumn(db, 'nodes', 'visibility')) { db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT'); - } catch { - /* already exists */ } - try { + if (hasTable(db, 'nodes')) { db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL'); - } catch { - /* nodes table may not exist yet */ - } - try { - db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)'); - } catch { - /* already exists */ - } - try { - db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)'); - } catch { - /* already exists */ } + db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)'); } From b691fcc90b9cc9757997cfc076af00b5dd756473 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:21:22 -0600 Subject: [PATCH 04/14] fix: replace empty catch blocks in domain analysis layer Add debug() logging to 10 empty catch blocks across context.js, symbol-lookup.js, exports.js, impact.js, and module-map.js. All catches retain their fallback behavior but failures are now observable via debug logging. Impact: 6 functions changed, 18 affected --- src/domain/analysis/context.js | 13 +++++++------ src/domain/analysis/exports.js | 5 +++-- src/domain/analysis/impact.js | 13 +++++++------ src/domain/analysis/module-map.js | 9 +++++---- src/domain/analysis/symbol-lookup.js | 4 +++- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/domain/analysis/context.js b/src/domain/analysis/context.js index e3409208..a97e5419 100644 --- a/src/domain/analysis/context.js +++ b/src/domain/analysis/context.js @@ -13,6 +13,7 @@ import { getComplexityForNode, openReadonlyOrFail, } from '../../db/index.js'; +import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { createFileLinesReader, @@ -142,8 +143,8 @@ function explainFunctionImpl(db, target, noTests, getFileLines) { halsteadVolume: cRow.halstead_volume || 0, }; } - } catch { - /* table may not exist */ + } catch (e) { + debug(`complexity lookup failed for node ${node.id}: ${e.message}`); } return { @@ -311,8 +312,8 @@ export function contextData(name, customDbPath, opts = {}) { halsteadVolume: cRow.halstead_volume || 0, }; } - } catch { - /* table may not exist */ + } catch (e) { + debug(`complexity lookup failed for node ${node.id}: ${e.message}`); } // Children (parameters, properties, constants) @@ -324,8 +325,8 @@ export function contextData(name, customDbPath, opts = {}) { line: c.line, endLine: c.end_line || null, })); - } catch { - /* parent_id column may not exist */ + } catch (e) { + debug(`findNodeChildren failed for node ${node.id}: ${e.message}`); } return { diff --git a/src/domain/analysis/exports.js b/src/domain/analysis/exports.js index 9af6b807..7bebac40 100644 --- a/src/domain/analysis/exports.js +++ b/src/domain/analysis/exports.js @@ -6,6 +6,7 @@ import { findNodesByFile, openReadonlyOrFail, } from '../../db/index.js'; +import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { createFileLinesReader, @@ -60,8 +61,8 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) { try { db.prepare('SELECT exported FROM nodes LIMIT 0').raw(); hasExportedCol = true; - } catch { - /* old DB without exported column */ + } catch (e) { + debug(`exported column not available, using fallback: ${e.message}`); } return fileNodes.map((fn) => { diff --git a/src/domain/analysis/impact.js b/src/domain/analysis/impact.js index 736d76e0..bd3bbe1d 100644 --- a/src/domain/analysis/impact.js +++ b/src/domain/analysis/impact.js @@ -13,6 +13,7 @@ import { evaluateBoundaries } from '../../features/boundaries.js'; import { coChangeForFiles } from '../../features/cochange.js'; import { ownersForFiles } from '../../features/owners.js'; import { loadConfig } from '../../infrastructure/config.js'; +import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { normalizeSymbol } from '../../shared/normalize.js'; import { paginateResult } from '../../shared/paginate.js'; @@ -289,8 +290,8 @@ export function diffImpactData(customDbPath, opts = {}) { }); // Exclude files already found via static analysis historicallyCoupled = coResults.filter((r) => !affectedFiles.has(r.file)); - } catch { - /* co_changes table doesn't exist — skip silently */ + } catch (e) { + debug(`co_changes lookup skipped: ${e.message}`); } // Look up CODEOWNERS for changed + affected files @@ -305,8 +306,8 @@ export function diffImpactData(customDbPath, opts = {}) { suggestedReviewers: ownerResult.suggestedReviewers, }; } - } catch { - /* CODEOWNERS missing or unreadable — skip silently */ + } catch (e) { + debug(`CODEOWNERS lookup skipped: ${e.message}`); } // Check boundary violations scoped to changed files @@ -323,8 +324,8 @@ export function diffImpactData(customDbPath, opts = {}) { boundaryViolations = result.violations; boundaryViolationCount = result.violationCount; } - } catch { - /* boundary check failed — skip silently */ + } catch (e) { + debug(`boundary check skipped: ${e.message}`); } const base = { diff --git a/src/domain/analysis/module-map.js b/src/domain/analysis/module-map.js index e6aa0936..d2bc613b 100644 --- a/src/domain/analysis/module-map.js +++ b/src/domain/analysis/module-map.js @@ -1,5 +1,6 @@ import path from 'node:path'; import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js'; +import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { findCycles } from '../graph/cycles.js'; import { LANGUAGE_REGISTRY } from '../parser.js'; @@ -193,8 +194,8 @@ export function statsData(customDbPath, opts = {}) { builtAt: meta.built_at || null, }; } - } catch { - /* embeddings table may not exist */ + } catch (e) { + debug(`embeddings lookup skipped: ${e.message}`); } // Graph quality metrics @@ -301,8 +302,8 @@ export function statsData(customDbPath, opts = {}) { minMI: +Math.min(...miValues).toFixed(1), }; } - } catch { - /* table may not exist in older DBs */ + } catch (e) { + debug(`complexity summary skipped: ${e.message}`); } return { diff --git a/src/domain/analysis/symbol-lookup.js b/src/domain/analysis/symbol-lookup.js index b272004a..312581cc 100644 --- a/src/domain/analysis/symbol-lookup.js +++ b/src/domain/analysis/symbol-lookup.js @@ -14,6 +14,7 @@ import { openReadonlyOrFail, Repository, } from '../../db/index.js'; +import { debug } from '../../infrastructure/logger.js'; import { isTestFile } from '../../infrastructure/test-filter.js'; import { ALL_SYMBOL_KINDS } from '../../shared/kinds.js'; import { getFileHash, normalizeSymbol } from '../../shared/normalize.js'; @@ -206,7 +207,8 @@ export function childrenData(name, customDbPath, opts = {}) { let children; try { children = findNodeChildren(db, node.id); - } catch { + } catch (e) { + debug(`findNodeChildren failed for node ${node.id}: ${e.message}`); children = []; } if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file)); From dadb383a8dea5b7be7ab7ea7ac7e705633de9314 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:22:28 -0600 Subject: [PATCH 05/14] fix: replace empty catch blocks in parser.js Add debug() logging to 6 empty catch blocks: 3 in disposeParsers() for WASM resource cleanup, 2 in ensureWasmTrees() for file read and parse failures, and 1 in getActiveEngine() for version lookup. Impact: 3 functions changed, 0 affected --- src/domain/parser.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/domain/parser.js b/src/domain/parser.js index fb41d473..476e6184 100644 --- a/src/domain/parser.js +++ b/src/domain/parser.js @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Language, Parser, Query } from 'web-tree-sitter'; -import { warn } from '../infrastructure/logger.js'; +import { debug, warn } from '../infrastructure/logger.js'; import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js'; // Re-export all extractors for backward compatibility @@ -116,29 +116,35 @@ export async function createParsers() { */ export function disposeParsers() { if (_cachedParsers) { - for (const [, parser] of _cachedParsers) { + for (const [id, parser] of _cachedParsers) { if (parser && typeof parser.delete === 'function') { try { parser.delete(); - } catch {} + } catch (e) { + debug(`Failed to dispose parser ${id}: ${e.message}`); + } } } _cachedParsers = null; } - for (const [, query] of _queryCache) { + for (const [id, query] of _queryCache) { if (query && typeof query.delete === 'function') { try { query.delete(); - } catch {} + } catch (e) { + debug(`Failed to dispose query ${id}: ${e.message}`); + } } } _queryCache.clear(); if (_cachedLanguages) { - for (const [, lang] of _cachedLanguages) { + for (const [id, lang] of _cachedLanguages) { if (lang && typeof lang.delete === 'function') { try { lang.delete(); - } catch {} + } catch (e) { + debug(`Failed to dispose language ${id}: ${e.message}`); + } } } _cachedLanguages = null; @@ -189,14 +195,15 @@ export async function ensureWasmTrees(fileSymbols, rootDir) { let code; try { code = fs.readFileSync(absPath, 'utf-8'); - } catch { + } catch (e) { + debug(`ensureWasmTrees: cannot read ${relPath}: ${e.message}`); continue; } try { symbols._tree = parser.parse(code); symbols._langId = entry.id; - } catch { - // skip files that fail to parse + } catch (e) { + debug(`ensureWasmTrees: parse failed for ${relPath}: ${e.message}`); } } } @@ -483,7 +490,9 @@ export function getActiveEngine(opts = {}) { if (native) { try { version = getNativePackageVersion() ?? version; - } catch {} + } catch (e) { + debug(`getNativePackageVersion failed: ${e.message}`); + } } return { name, version }; } From 22d94f4f70437a5c319431e0ee5a1e313ffdeef3 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:29:16 -0600 Subject: [PATCH 06/14] fix: replace empty catch blocks in features layer Add debug() logging to 9 empty catch blocks across complexity.js (5), cfg.js (2), and dataflow.js (2). All catches for file read and parse failures now log the error message before continuing. Impact: 4 functions changed, 2 affected --- src/features/cfg.js | 8 +++++--- src/features/complexity.js | 23 +++++++++++++---------- src/features/dataflow.js | 8 +++++--- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/features/cfg.js b/src/features/cfg.js index eff08652..ae1b8564 100644 --- a/src/features/cfg.js +++ b/src/features/cfg.js @@ -23,7 +23,7 @@ import { hasCfgTables, openReadonlyOrFail, } from '../db/index.js'; -import { info } from '../infrastructure/logger.js'; +import { debug, info } from '../infrastructure/logger.js'; import { paginateResult } from '../shared/paginate.js'; import { findNodes } from './shared/find-nodes.js'; @@ -149,7 +149,8 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) { let code; try { code = fs.readFileSync(absPath, 'utf-8'); - } catch { + } catch (e) { + debug(`cfg: cannot read ${relPath}: ${e.message}`); continue; } @@ -158,7 +159,8 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) { try { tree = parser.parse(code); - } catch { + } catch (e) { + debug(`cfg: parse failed for ${relPath}: ${e.message}`); continue; } } diff --git a/src/features/complexity.js b/src/features/complexity.js index c5cdf62e..12f5acf1 100644 --- a/src/features/complexity.js +++ b/src/features/complexity.js @@ -14,7 +14,7 @@ import { walkWithVisitors } from '../ast-analysis/visitor.js'; import { createComplexityVisitor } from '../ast-analysis/visitors/complexity-visitor.js'; import { getFunctionNodeId, openReadonlyOrFail } from '../db/index.js'; import { loadConfig } from '../infrastructure/config.js'; -import { info } from '../infrastructure/logger.js'; +import { debug, info } from '../infrastructure/logger.js'; import { isTestFile } from '../infrastructure/test-filter.js'; import { paginateResult } from '../shared/paginate.js'; @@ -401,7 +401,8 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp let code; try { code = fs.readFileSync(absPath, 'utf-8'); - } catch { + } catch (e) { + debug(`complexity: cannot read ${relPath}: ${e.message}`); continue; } @@ -410,7 +411,8 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp try { tree = parser.parse(code); - } catch { + } catch (e) { + debug(`complexity: parse failed for ${relPath}: ${e.message}`); continue; } } @@ -606,13 +608,14 @@ export function complexityData(customDbPath, opts = {}) { ORDER BY ${orderBy}`, ) .all(...params); - } catch { + } catch (e) { + debug(`complexity query failed (table may not exist): ${e.message}`); // Check if graph has nodes even though complexity table is missing/empty let hasGraph = false; try { hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0; - } catch { - /* ignore */ + } catch (e2) { + debug(`nodes table check failed: ${e2.message}`); } return { functions: [], summary: null, thresholds, hasGraph }; } @@ -701,8 +704,8 @@ export function complexityData(customDbPath, opts = {}) { ).length, }; } - } catch { - /* ignore */ + } catch (e) { + debug(`complexity summary query failed: ${e.message}`); } // When summary is null (no complexity rows), check if graph has nodes @@ -710,8 +713,8 @@ export function complexityData(customDbPath, opts = {}) { if (summary === null) { try { hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0; - } catch { - /* ignore */ + } catch (e) { + debug(`nodes table check failed: ${e.message}`); } } diff --git a/src/features/dataflow.js b/src/features/dataflow.js index 0f500b8f..695afa95 100644 --- a/src/features/dataflow.js +++ b/src/features/dataflow.js @@ -21,7 +21,7 @@ import { walkWithVisitors } from '../ast-analysis/visitor.js'; import { createDataflowVisitor } from '../ast-analysis/visitors/dataflow-visitor.js'; import { hasDataflowTable, openReadonlyOrFail } from '../db/index.js'; import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js'; -import { info } from '../infrastructure/logger.js'; +import { debug, info } from '../infrastructure/logger.js'; import { isTestFile } from '../infrastructure/test-filter.js'; import { paginateResult } from '../shared/paginate.js'; import { findNodes } from './shared/find-nodes.js'; @@ -141,7 +141,8 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) let code; try { code = fs.readFileSync(absPath, 'utf-8'); - } catch { + } catch (e) { + debug(`dataflow: cannot read ${relPath}: ${e.message}`); continue; } @@ -150,7 +151,8 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) try { tree = parser.parse(code); - } catch { + } catch (e) { + debug(`dataflow: parse failed for ${relPath}: ${e.message}`); continue; } } From 3b365347a3e9e3ce39b652d674b69db8e7458278 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:39:16 -0600 Subject: [PATCH 07/14] refactor: decompose extractSymbolsWalk into per-category handlers Split the monolithic walkJavaScriptNode switch (13 cases, cognitive 228) into 11 focused handler functions. The dispatcher is now a thin switch that delegates to handleFunctionDecl, handleClassDecl, handleMethodDef, handleInterfaceDecl, handleTypeAliasDecl, handleVariableDecl, handleEnumDecl, handleCallExpr, handleImportStmt, handleExportStmt, and handleExpressionStmt. The expression_statement case now reuses the existing handleCommonJSAssignment helper, eliminating ~50 lines of duplication. Worst handler complexity: handleVariableDecl (cognitive 20), down from the original monolithic function (cognitive 279). Impact: 13 functions changed, 3 affected --- src/extractors/javascript.js | 578 +++++++++++++++++------------------ 1 file changed, 274 insertions(+), 304 deletions(-) diff --git a/src/extractors/javascript.js b/src/extractors/javascript.js index a2d9e7b1..997c8ea6 100644 --- a/src/extractors/javascript.js +++ b/src/extractors/javascript.js @@ -320,333 +320,303 @@ function handleCommonJSAssignment(left, right, node, imports) { // ── Manual tree walk (fallback when Query not available) ──────────────────── function extractSymbolsWalk(tree) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; - - function walkJavaScriptNode(node) { - switch (node.type) { - case 'function_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const fnChildren = extractParameters(node); - definitions.push({ - name: nameNode.text, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: fnChildren.length > 0 ? fnChildren : undefined, - }); - } - break; - } + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; + + walkJavaScriptNode(tree.rootNode, ctx); + return ctx; +} - case 'class_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const className = nameNode.text; - const startLine = node.startPosition.row + 1; - const clsChildren = extractClassProperties(node); - definitions.push({ - name: className, - kind: 'class', - line: startLine, - endLine: nodeEndLine(node), - children: clsChildren.length > 0 ? clsChildren : undefined, - }); - const heritage = node.childForFieldName('heritage') || findChild(node, 'class_heritage'); - if (heritage) { - const superName = extractSuperclass(heritage); - if (superName) { - classes.push({ name: className, extends: superName, line: startLine }); - } - const implementsList = extractImplements(heritage); - for (const iface of implementsList) { - classes.push({ name: className, implements: iface, line: startLine }); - } - } - } - break; - } +function walkJavaScriptNode(node, ctx) { + switch (node.type) { + case 'function_declaration': + handleFunctionDecl(node, ctx); + break; + case 'class_declaration': + handleClassDecl(node, ctx); + break; + case 'method_definition': + handleMethodDef(node, ctx); + break; + case 'interface_declaration': + handleInterfaceDecl(node, ctx); + break; + case 'type_alias_declaration': + handleTypeAliasDecl(node, ctx); + break; + case 'lexical_declaration': + case 'variable_declaration': + handleVariableDecl(node, ctx); + break; + case 'enum_declaration': + handleEnumDecl(node, ctx); + break; + case 'call_expression': + handleCallExpr(node, ctx); + break; + case 'import_statement': + handleImportStmt(node, ctx); + break; + case 'export_statement': + handleExportStmt(node, ctx); + break; + case 'expression_statement': + handleExpressionStmt(node, ctx); + break; + } - case 'method_definition': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const methChildren = extractParameters(node); - const methVis = extractVisibility(node); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: methChildren.length > 0 ? methChildren : undefined, - visibility: methVis, - }); - } - break; - } + for (let i = 0; i < node.childCount; i++) { + walkJavaScriptNode(node.child(i), ctx); + } +} - case 'interface_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'interface', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - const body = - node.childForFieldName('body') || - findChild(node, 'interface_body') || - findChild(node, 'object_type'); - if (body) { - extractInterfaceMethods(body, nameNode.text, definitions); - } - } - break; - } +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - case 'type_alias_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'type', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - } - break; - } +function handleFunctionDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const fnChildren = extractParameters(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: fnChildren.length > 0 ? fnChildren : undefined, + }); + } +} - case 'lexical_declaration': - case 'variable_declaration': { - const isConst = node.text.startsWith('const '); - for (let i = 0; i < node.childCount; i++) { - const declarator = node.child(i); - if (declarator && declarator.type === 'variable_declarator') { - const nameN = declarator.childForFieldName('name'); - const valueN = declarator.childForFieldName('value'); - if (nameN && valueN) { - const valType = valueN.type; - if ( - valType === 'arrow_function' || - valType === 'function_expression' || - valType === 'function' - ) { - const varFnChildren = extractParameters(valueN); - definitions.push({ - name: nameN.text, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(valueN), - children: varFnChildren.length > 0 ? varFnChildren : undefined, - }); - } else if (isConst && nameN.type === 'identifier' && isConstantValue(valueN)) { - definitions.push({ - name: nameN.text, - kind: 'constant', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - } - } else if (isConst && nameN && nameN.type === 'identifier' && !valueN) { - // const with no value (shouldn't happen but be safe) - } - } - } - break; - } +function handleClassDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const className = nameNode.text; + const startLine = node.startPosition.row + 1; + const clsChildren = extractClassProperties(node); + ctx.definitions.push({ + name: className, + kind: 'class', + line: startLine, + endLine: nodeEndLine(node), + children: clsChildren.length > 0 ? clsChildren : undefined, + }); + const heritage = node.childForFieldName('heritage') || findChild(node, 'class_heritage'); + if (heritage) { + const superName = extractSuperclass(heritage); + if (superName) { + ctx.classes.push({ name: className, extends: superName, line: startLine }); + } + const implementsList = extractImplements(heritage); + for (const iface of implementsList) { + ctx.classes.push({ name: className, implements: iface, line: startLine }); + } + } +} - case 'enum_declaration': { - // TypeScript enum - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const enumChildren = []; - const body = node.childForFieldName('body') || findChild(node, 'enum_body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const member = body.child(i); - if (!member) continue; - if (member.type === 'enum_assignment' || member.type === 'property_identifier') { - const mName = member.childForFieldName('name') || member.child(0); - if (mName) { - enumChildren.push({ - name: mName.text, - kind: 'constant', - line: member.startPosition.row + 1, - }); - } - } - } - } - definitions.push({ - name: nameNode.text, - kind: 'enum', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: enumChildren.length > 0 ? enumChildren : undefined, - }); - } - break; - } +function handleMethodDef(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const parentClass = findParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const methChildren = extractParameters(node); + const methVis = extractVisibility(node); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: methChildren.length > 0 ? methChildren : undefined, + visibility: methVis, + }); + } +} - case 'call_expression': { - const fn = node.childForFieldName('function'); - if (fn) { - // Dynamic import(): import('./foo.js') → extract as an import entry - if (fn.type === 'import') { - const args = node.childForFieldName('arguments') || findChild(node, 'arguments'); - if (args) { - const strArg = findChild(args, 'string'); - if (strArg) { - const modPath = strArg.text.replace(/['"]/g, ''); - // Extract destructured names from parent context: - // const { a, b } = await import('./foo.js') - // (standalone import('./foo.js').then(...) calls produce an edge with empty names) - const names = extractDynamicImportNames(node); - imports.push({ - source: modPath, - names, - line: node.startPosition.row + 1, - dynamicImport: true, - }); - } else { - debug( - `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`, - ); - } - } - } else { - const callInfo = extractCallInfo(fn, node); - if (callInfo) calls.push(callInfo); - if (fn.type === 'member_expression') { - const cbDef = extractCallbackDefinition(node, fn); - if (cbDef) definitions.push(cbDef); - } - } - } - break; - } +function handleInterfaceDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + const body = + node.childForFieldName('body') || + findChild(node, 'interface_body') || + findChild(node, 'object_type'); + if (body) { + extractInterfaceMethods(body, nameNode.text, ctx.definitions); + } +} - case 'import_statement': { - const isTypeOnly = node.text.startsWith('import type'); - const source = node.childForFieldName('source') || findChild(node, 'string'); - if (source) { - const modPath = source.text.replace(/['"]/g, ''); - const names = extractImportNames(node); - imports.push({ - source: modPath, - names, +function handleTypeAliasDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + ctx.definitions.push({ + name: nameNode.text, + kind: 'type', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + } +} + +function handleVariableDecl(node, ctx) { + const isConst = node.text.startsWith('const '); + for (let i = 0; i < node.childCount; i++) { + const declarator = node.child(i); + if (declarator && declarator.type === 'variable_declarator') { + const nameN = declarator.childForFieldName('name'); + const valueN = declarator.childForFieldName('value'); + if (nameN && valueN) { + const valType = valueN.type; + if ( + valType === 'arrow_function' || + valType === 'function_expression' || + valType === 'function' + ) { + const varFnChildren = extractParameters(valueN); + ctx.definitions.push({ + name: nameN.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(valueN), + children: varFnChildren.length > 0 ? varFnChildren : undefined, + }); + } else if (isConst && nameN.type === 'identifier' && isConstantValue(valueN)) { + ctx.definitions.push({ + name: nameN.text, + kind: 'constant', line: node.startPosition.row + 1, - typeOnly: isTypeOnly, + endLine: nodeEndLine(node), }); } - break; } + } + } +} - case 'export_statement': { - const exportLine = node.startPosition.row + 1; - const decl = node.childForFieldName('declaration'); - if (decl) { - const declType = decl.type; - const kindMap = { - function_declaration: 'function', - class_declaration: 'class', - interface_declaration: 'interface', - type_alias_declaration: 'type', - }; - const kind = kindMap[declType]; - if (kind) { - const n = decl.childForFieldName('name'); - if (n) exports.push({ name: n.text, kind, line: exportLine }); - } - } - const source = node.childForFieldName('source') || findChild(node, 'string'); - if (source && !decl) { - const modPath = source.text.replace(/['"]/g, ''); - const reexportNames = extractImportNames(node); - const nodeText = node.text; - const isWildcard = nodeText.includes('export *') || nodeText.includes('export*'); - imports.push({ - source: modPath, - names: reexportNames, - line: exportLine, - reexport: true, - wildcardReexport: isWildcard && reexportNames.length === 0, +function handleEnumDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const enumChildren = []; + const body = node.childForFieldName('body') || findChild(node, 'enum_body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const member = body.child(i); + if (!member) continue; + if (member.type === 'enum_assignment' || member.type === 'property_identifier') { + const mName = member.childForFieldName('name') || member.child(0); + if (mName) { + enumChildren.push({ + name: mName.text, + kind: 'constant', + line: member.startPosition.row + 1, }); } - break; } + } + } + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: enumChildren.length > 0 ? enumChildren : undefined, + }); +} - case 'expression_statement': { - const expr = node.child(0); - if (expr && expr.type === 'assignment_expression') { - const left = expr.childForFieldName('left'); - const right = expr.childForFieldName('right'); - if (left && right) { - const leftText = left.text; - if (leftText.startsWith('module.exports') || leftText === 'exports') { - if (right.type === 'call_expression') { - const fn = right.childForFieldName('function'); - const args = right.childForFieldName('arguments') || findChild(right, 'arguments'); - if (fn && fn.text === 'require' && args) { - const strArg = findChild(args, 'string'); - if (strArg) { - imports.push({ - source: strArg.text.replace(/['"]/g, ''), - names: [], - line: node.startPosition.row + 1, - reexport: true, - wildcardReexport: true, - }); - } - } - } - if (right.type === 'object') { - for (let ci = 0; ci < right.childCount; ci++) { - const child = right.child(ci); - if (child && child.type === 'spread_element') { - const spreadExpr = child.child(1) || child.childForFieldName('value'); - if (spreadExpr && spreadExpr.type === 'call_expression') { - const fn2 = spreadExpr.childForFieldName('function'); - const args2 = - spreadExpr.childForFieldName('arguments') || - findChild(spreadExpr, 'arguments'); - if (fn2 && fn2.text === 'require' && args2) { - const strArg2 = findChild(args2, 'string'); - if (strArg2) { - imports.push({ - source: strArg2.text.replace(/['"]/g, ''), - names: [], - line: node.startPosition.row + 1, - reexport: true, - wildcardReexport: true, - }); - } - } - } - } - } - } - } - } - } - break; +function handleCallExpr(node, ctx) { + const fn = node.childForFieldName('function'); + if (!fn) return; + if (fn.type === 'import') { + const args = node.childForFieldName('arguments') || findChild(node, 'arguments'); + if (args) { + const strArg = findChild(args, 'string'); + if (strArg) { + const modPath = strArg.text.replace(/['"]/g, ''); + const names = extractDynamicImportNames(node); + ctx.imports.push({ + source: modPath, + names, + line: node.startPosition.row + 1, + dynamicImport: true, + }); + } else { + debug( + `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`, + ); } } + } else { + const callInfo = extractCallInfo(fn, node); + if (callInfo) ctx.calls.push(callInfo); + if (fn.type === 'member_expression') { + const cbDef = extractCallbackDefinition(node, fn); + if (cbDef) ctx.definitions.push(cbDef); + } + } +} - for (let i = 0; i < node.childCount; i++) { - walkJavaScriptNode(node.child(i)); +function handleImportStmt(node, ctx) { + const isTypeOnly = node.text.startsWith('import type'); + const source = node.childForFieldName('source') || findChild(node, 'string'); + if (source) { + const modPath = source.text.replace(/['"]/g, ''); + const names = extractImportNames(node); + ctx.imports.push({ + source: modPath, + names, + line: node.startPosition.row + 1, + typeOnly: isTypeOnly, + }); + } +} + +function handleExportStmt(node, ctx) { + const exportLine = node.startPosition.row + 1; + const decl = node.childForFieldName('declaration'); + if (decl) { + const declType = decl.type; + const kindMap = { + function_declaration: 'function', + class_declaration: 'class', + interface_declaration: 'interface', + type_alias_declaration: 'type', + }; + const kind = kindMap[declType]; + if (kind) { + const n = decl.childForFieldName('name'); + if (n) ctx.exports.push({ name: n.text, kind, line: exportLine }); } } + const source = node.childForFieldName('source') || findChild(node, 'string'); + if (source && !decl) { + const modPath = source.text.replace(/['"]/g, ''); + const reexportNames = extractImportNames(node); + const nodeText = node.text; + const isWildcard = nodeText.includes('export *') || nodeText.includes('export*'); + ctx.imports.push({ + source: modPath, + names: reexportNames, + line: exportLine, + reexport: true, + wildcardReexport: isWildcard && reexportNames.length === 0, + }); + } +} - walkJavaScriptNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function handleExpressionStmt(node, ctx) { + 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); + } } // ── Child extraction helpers ──────────────────────────────────────────────── From e1d7ee03846d70178fa75db4145482375687d12b Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:41:20 -0600 Subject: [PATCH 08/14] refactor: decompose extractPythonSymbols into per-category handlers Split walkPythonNode switch into 7 focused handlers: handlePyFunctionDef, handlePyClassDef, handlePyCall, handlePyImport, handlePyExpressionStmt, handlePyImportFrom, plus the decorated_definition inline dispatch. Moved extractPythonParameters, extractPythonClassProperties, walkInitBody, and findPythonParentClass from closures to module-scope functions. Impact: 12 functions changed, 5 affected --- src/extractors/python.js | 502 ++++++++++++++++++++------------------- 1 file changed, 252 insertions(+), 250 deletions(-) diff --git a/src/extractors/python.js b/src/extractors/python.js index 968dbacb..053a07ca 100644 --- a/src/extractors/python.js +++ b/src/extractors/python.js @@ -4,292 +4,294 @@ import { findChild, nodeEndLine, pythonVisibility } from './helpers.js'; * Extract symbols from Python files. */ export function extractPythonSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function walkPythonNode(node) { - switch (node.type) { - case 'function_definition': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const decorators = []; - if (node.previousSibling && node.previousSibling.type === 'decorator') { - decorators.push(node.previousSibling.text); - } - const parentClass = findPythonParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const kind = parentClass ? 'method' : 'function'; - const fnChildren = extractPythonParameters(node); - definitions.push({ - name: fullName, - kind, - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - decorators, - children: fnChildren.length > 0 ? fnChildren : undefined, - visibility: pythonVisibility(nameNode.text), - }); - } - break; - } + walkPythonNode(tree.rootNode, ctx); + return ctx; +} - case 'class_definition': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const clsChildren = extractPythonClassProperties(node); - definitions.push({ - name: nameNode.text, - kind: 'class', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: clsChildren.length > 0 ? clsChildren : undefined, - }); - const superclasses = - node.childForFieldName('superclasses') || findChild(node, 'argument_list'); - if (superclasses) { - for (let i = 0; i < superclasses.childCount; i++) { - const child = superclasses.child(i); - if (child && child.type === 'identifier') { - classes.push({ - name: nameNode.text, - extends: child.text, - line: node.startPosition.row + 1, - }); - } - } - } - } - break; - } +function walkPythonNode(node, ctx) { + switch (node.type) { + case 'function_definition': + handlePyFunctionDef(node, ctx); + break; + case 'class_definition': + handlePyClassDef(node, ctx); + break; + case 'decorated_definition': + for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i), ctx); + return; + case 'call': + handlePyCall(node, ctx); + break; + case 'import_statement': + handlePyImport(node, ctx); + break; + case 'expression_statement': + handlePyExpressionStmt(node, ctx); + break; + case 'import_from_statement': + handlePyImportFrom(node, ctx); + break; + } - case 'decorated_definition': { - for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i)); - return; - } + for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i), ctx); +} - case 'call': { - const fn = node.childForFieldName('function'); - if (fn) { - let callName = null; - let receiver; - if (fn.type === 'identifier') callName = fn.text; - else if (fn.type === 'attribute') { - const attr = fn.childForFieldName('attribute'); - if (attr) callName = attr.text; - const obj = fn.childForFieldName('object'); - if (obj) receiver = obj.text; - } - if (callName) { - const call = { name: callName, line: node.startPosition.row + 1 }; - if (receiver) call.receiver = receiver; - calls.push(call); - } - } - break; - } +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - case 'import_statement': { - const names = []; - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (child && (child.type === 'dotted_name' || child.type === 'aliased_import')) { - const name = - child.type === 'aliased_import' - ? (child.childForFieldName('alias') || child.childForFieldName('name'))?.text - : child.text; - if (name) names.push(name); - } - } - if (names.length > 0) - imports.push({ - source: names[0], - names, - line: node.startPosition.row + 1, - pythonImport: true, - }); - break; - } +function handlePyFunctionDef(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const decorators = []; + if (node.previousSibling && node.previousSibling.type === 'decorator') { + decorators.push(node.previousSibling.text); + } + const parentClass = findPythonParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const kind = parentClass ? 'method' : 'function'; + const fnChildren = extractPythonParameters(node); + ctx.definitions.push({ + name: fullName, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + decorators, + children: fnChildren.length > 0 ? fnChildren : undefined, + visibility: pythonVisibility(nameNode.text), + }); +} - case 'expression_statement': { - // Module-level UPPER_CASE assignments → constants - if (node.parent && node.parent.type === 'module') { - const assignment = findChild(node, 'assignment'); - if (assignment) { - const left = assignment.childForFieldName('left'); - if (left && left.type === 'identifier' && /^[A-Z_][A-Z0-9_]*$/.test(left.text)) { - definitions.push({ - name: left.text, - kind: 'constant', - line: node.startPosition.row + 1, - }); - } - } - } - break; +function handlePyClassDef(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const clsChildren = extractPythonClassProperties(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: clsChildren.length > 0 ? clsChildren : undefined, + }); + const superclasses = node.childForFieldName('superclasses') || findChild(node, 'argument_list'); + if (superclasses) { + for (let i = 0; i < superclasses.childCount; i++) { + const child = superclasses.child(i); + if (child && child.type === 'identifier') { + ctx.classes.push({ + name: nameNode.text, + extends: child.text, + line: node.startPosition.row + 1, + }); } + } + } +} - case 'import_from_statement': { - let source = ''; - const names = []; - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (!child) continue; - if (child.type === 'dotted_name' || child.type === 'relative_import') { - if (!source) source = child.text; - else names.push(child.text); - } - if (child.type === 'aliased_import') { - const n = child.childForFieldName('name') || child.child(0); - if (n) names.push(n.text); - } - if (child.type === 'wildcard_import') names.push('*'); - } - if (source) - imports.push({ source, names, line: node.startPosition.row + 1, pythonImport: true }); - break; +function handlePyCall(node, ctx) { + const fn = node.childForFieldName('function'); + if (!fn) return; + let callName = null; + let receiver; + if (fn.type === 'identifier') callName = fn.text; + else if (fn.type === 'attribute') { + const attr = fn.childForFieldName('attribute'); + if (attr) callName = attr.text; + const obj = fn.childForFieldName('object'); + if (obj) receiver = obj.text; + } + if (callName) { + const call = { name: callName, line: node.startPosition.row + 1 }; + if (receiver) call.receiver = receiver; + ctx.calls.push(call); + } +} + +function handlePyImport(node, ctx) { + const names = []; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child && (child.type === 'dotted_name' || child.type === 'aliased_import')) { + const name = + child.type === 'aliased_import' + ? (child.childForFieldName('alias') || child.childForFieldName('name'))?.text + : child.text; + if (name) names.push(name); + } + } + if (names.length > 0) + ctx.imports.push({ + source: names[0], + names, + line: node.startPosition.row + 1, + pythonImport: true, + }); +} + +function handlePyExpressionStmt(node, ctx) { + if (node.parent && node.parent.type === 'module') { + const assignment = findChild(node, 'assignment'); + if (assignment) { + const left = assignment.childForFieldName('left'); + if (left && left.type === 'identifier' && /^[A-Z_][A-Z0-9_]*$/.test(left.text)) { + ctx.definitions.push({ + name: left.text, + kind: 'constant', + line: node.startPosition.row + 1, + }); } } + } +} - for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i)); +function handlePyImportFrom(node, ctx) { + let source = ''; + const names = []; + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + if (child.type === 'dotted_name' || child.type === 'relative_import') { + if (!source) source = child.text; + else names.push(child.text); + } + if (child.type === 'aliased_import') { + const n = child.childForFieldName('name') || child.child(0); + if (n) names.push(n.text); + } + if (child.type === 'wildcard_import') names.push('*'); } + if (source) + ctx.imports.push({ source, names, line: node.startPosition.row + 1, pythonImport: true }); +} - function extractPythonParameters(fnNode) { - const params = []; - const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters'); - if (!paramsNode) return params; - for (let i = 0; i < paramsNode.childCount; i++) { - const child = paramsNode.child(i); - if (!child) continue; - const t = child.type; - if (t === 'identifier') { - params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 }); - } else if ( - t === 'typed_parameter' || - t === 'default_parameter' || - t === 'typed_default_parameter' - ) { - const nameNode = child.childForFieldName('name') || child.child(0); - if (nameNode && nameNode.type === 'identifier') { - params.push({ - name: nameNode.text, - kind: 'parameter', - line: child.startPosition.row + 1, - }); - } - } else if (t === 'list_splat_pattern' || t === 'dictionary_splat_pattern') { - // *args, **kwargs - for (let j = 0; j < child.childCount; j++) { - const inner = child.child(j); - if (inner && inner.type === 'identifier') { - params.push({ name: inner.text, kind: 'parameter', line: child.startPosition.row + 1 }); - break; - } +// ── Python-specific helpers ───────────────────────────────────────────────── + +function extractPythonParameters(fnNode) { + const params = []; + const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters'); + if (!paramsNode) return params; + for (let i = 0; i < paramsNode.childCount; i++) { + const child = paramsNode.child(i); + if (!child) continue; + const t = child.type; + if (t === 'identifier') { + params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 }); + } else if ( + t === 'typed_parameter' || + t === 'default_parameter' || + t === 'typed_default_parameter' + ) { + const nameNode = child.childForFieldName('name') || child.child(0); + if (nameNode && nameNode.type === 'identifier') { + params.push({ + name: nameNode.text, + kind: 'parameter', + line: child.startPosition.row + 1, + }); + } + } else if (t === 'list_splat_pattern' || t === 'dictionary_splat_pattern') { + for (let j = 0; j < child.childCount; j++) { + const inner = child.child(j); + if (inner && inner.type === 'identifier') { + params.push({ name: inner.text, kind: 'parameter', line: child.startPosition.row + 1 }); + break; } } } - return params; } + return params; +} - function extractPythonClassProperties(classNode) { - const props = []; - const seen = new Set(); - const body = classNode.childForFieldName('body') || findChild(classNode, 'block'); - if (!body) return props; +function extractPythonClassProperties(classNode) { + const props = []; + const seen = new Set(); + const body = classNode.childForFieldName('body') || findChild(classNode, 'block'); + if (!body) return props; - for (let i = 0; i < body.childCount; i++) { - const child = body.child(i); - if (!child) continue; + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (!child) continue; - // Direct class attribute assignments: x = 5 - if (child.type === 'expression_statement') { - const assignment = findChild(child, 'assignment'); - if (assignment) { - const left = assignment.childForFieldName('left'); - if (left && left.type === 'identifier' && !seen.has(left.text)) { - seen.add(left.text); - props.push({ - name: left.text, - kind: 'property', - line: child.startPosition.row + 1, - visibility: pythonVisibility(left.text), - }); - } + if (child.type === 'expression_statement') { + const assignment = findChild(child, 'assignment'); + if (assignment) { + const left = assignment.childForFieldName('left'); + if (left && left.type === 'identifier' && !seen.has(left.text)) { + seen.add(left.text); + props.push({ + name: left.text, + kind: 'property', + line: child.startPosition.row + 1, + visibility: pythonVisibility(left.text), + }); } } + } - // __init__ method: self.x = ... assignments - if (child.type === 'function_definition') { - const fnName = child.childForFieldName('name'); - if (fnName && fnName.text === '__init__') { - const initBody = child.childForFieldName('body') || findChild(child, 'block'); - if (initBody) { - walkInitBody(initBody, seen, props); - } + if (child.type === 'function_definition') { + const fnName = child.childForFieldName('name'); + if (fnName && fnName.text === '__init__') { + const initBody = child.childForFieldName('body') || findChild(child, 'block'); + if (initBody) { + walkInitBody(initBody, seen, props); } } + } - // decorated __init__ - if (child.type === 'decorated_definition') { - for (let j = 0; j < child.childCount; j++) { - const inner = child.child(j); - if (inner && inner.type === 'function_definition') { - const fnName = inner.childForFieldName('name'); - if (fnName && fnName.text === '__init__') { - const initBody = inner.childForFieldName('body') || findChild(inner, 'block'); - if (initBody) { - walkInitBody(initBody, seen, props); - } + if (child.type === 'decorated_definition') { + for (let j = 0; j < child.childCount; j++) { + const inner = child.child(j); + if (inner && inner.type === 'function_definition') { + const fnName = inner.childForFieldName('name'); + if (fnName && fnName.text === '__init__') { + const initBody = inner.childForFieldName('body') || findChild(inner, 'block'); + if (initBody) { + walkInitBody(initBody, seen, props); } } } } } - return props; } + return props; +} - function walkInitBody(bodyNode, seen, props) { - for (let i = 0; i < bodyNode.childCount; i++) { - const stmt = bodyNode.child(i); - if (!stmt || stmt.type !== 'expression_statement') continue; - const assignment = findChild(stmt, 'assignment'); - if (!assignment) continue; - const left = assignment.childForFieldName('left'); - if (!left || left.type !== 'attribute') continue; - const obj = left.childForFieldName('object'); - const attr = left.childForFieldName('attribute'); - if ( - obj && - obj.text === 'self' && - attr && - attr.type === 'identifier' && - !seen.has(attr.text) - ) { - seen.add(attr.text); - props.push({ - name: attr.text, - kind: 'property', - line: stmt.startPosition.row + 1, - visibility: pythonVisibility(attr.text), - }); - } +function walkInitBody(bodyNode, seen, props) { + for (let i = 0; i < bodyNode.childCount; i++) { + const stmt = bodyNode.child(i); + if (!stmt || stmt.type !== 'expression_statement') continue; + const assignment = findChild(stmt, 'assignment'); + if (!assignment) continue; + const left = assignment.childForFieldName('left'); + if (!left || left.type !== 'attribute') continue; + const obj = left.childForFieldName('object'); + const attr = left.childForFieldName('attribute'); + if (obj && obj.text === 'self' && attr && attr.type === 'identifier' && !seen.has(attr.text)) { + seen.add(attr.text); + props.push({ + name: attr.text, + kind: 'property', + line: stmt.startPosition.row + 1, + visibility: pythonVisibility(attr.text), + }); } } +} - function findPythonParentClass(node) { - let current = node.parent; - while (current) { - if (current.type === 'class_definition') { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - current = current.parent; +function findPythonParentClass(node) { + let current = node.parent; + while (current) { + if (current.type === 'class_definition') { + const nameNode = current.childForFieldName('name'); + return nameNode ? nameNode.text : null; } - return null; + current = current.parent; } - - walkPythonNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; + return null; } From 3a656bb089cca1e595d3a28331fb5303d5c11a16 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:42:33 -0600 Subject: [PATCH 09/14] refactor: decompose extractJavaSymbols into per-category handlers Split walkJavaNode switch into 8 focused handlers plus an extractJavaInterfaces helper. Moved findJavaParentClass to module scope. The class_declaration case (deepest nesting in the file) is now split between handleJavaClassDecl and extractJavaInterfaces. Impact: 12 functions changed, 5 affected --- src/extractors/java.js | 418 +++++++++++++++++++++-------------------- 1 file changed, 211 insertions(+), 207 deletions(-) diff --git a/src/extractors/java.js b/src/extractors/java.js index 2bf0bb28..9da313c1 100644 --- a/src/extractors/java.js +++ b/src/extractors/java.js @@ -4,239 +4,243 @@ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js' * Extract symbols from Java files. */ export function extractJavaSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function findJavaParentClass(node) { - let current = node.parent; - while (current) { - if ( - current.type === 'class_declaration' || - current.type === 'enum_declaration' || - current.type === 'interface_declaration' - ) { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - current = current.parent; - } - return null; + walkJavaNode(tree.rootNode, ctx); + return ctx; +} + +function walkJavaNode(node, ctx) { + switch (node.type) { + case 'class_declaration': + handleJavaClassDecl(node, ctx); + break; + case 'interface_declaration': + handleJavaInterfaceDecl(node, ctx); + break; + case 'enum_declaration': + handleJavaEnumDecl(node, ctx); + break; + case 'method_declaration': + handleJavaMethodDecl(node, ctx); + break; + case 'constructor_declaration': + handleJavaConstructorDecl(node, ctx); + break; + case 'import_declaration': + handleJavaImportDecl(node, ctx); + break; + case 'method_invocation': + handleJavaMethodInvocation(node, ctx); + break; + case 'object_creation_expression': + handleJavaObjectCreation(node, ctx); + break; } - function walkJavaNode(node) { - switch (node.type) { - case 'class_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const classChildren = extractClassFields(node); - definitions.push({ - name: nameNode.text, - kind: 'class', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: classChildren.length > 0 ? classChildren : undefined, - }); + for (let i = 0; i < node.childCount; i++) walkJavaNode(node.child(i), ctx); +} - const superclass = node.childForFieldName('superclass'); - if (superclass) { - for (let i = 0; i < superclass.childCount; i++) { - const child = superclass.child(i); - if ( - child && - (child.type === 'type_identifier' || - child.type === 'identifier' || - child.type === 'generic_type') - ) { - const superName = child.type === 'generic_type' ? child.child(0)?.text : child.text; - if (superName) - classes.push({ - name: nameNode.text, - extends: superName, - line: node.startPosition.row + 1, - }); - break; - } - } - } +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - const interfaces = node.childForFieldName('interfaces'); - if (interfaces) { - for (let i = 0; i < interfaces.childCount; i++) { - const child = interfaces.child(i); - if ( - child && - (child.type === 'type_identifier' || - child.type === 'identifier' || - child.type === 'type_list' || - child.type === 'generic_type') - ) { - if (child.type === 'type_list') { - for (let j = 0; j < child.childCount; j++) { - const t = child.child(j); - if ( - t && - (t.type === 'type_identifier' || - t.type === 'identifier' || - t.type === 'generic_type') - ) { - const ifaceName = t.type === 'generic_type' ? t.child(0)?.text : t.text; - if (ifaceName) - classes.push({ - name: nameNode.text, - implements: ifaceName, - line: node.startPosition.row + 1, - }); - } - } - } else { - const ifaceName = - child.type === 'generic_type' ? child.child(0)?.text : child.text; - if (ifaceName) - classes.push({ - name: nameNode.text, - implements: ifaceName, - line: node.startPosition.row + 1, - }); - } - } - } - } - } - break; - } +function handleJavaClassDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const classChildren = extractClassFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: classChildren.length > 0 ? classChildren : undefined, + }); - case 'interface_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ + const superclass = node.childForFieldName('superclass'); + if (superclass) { + for (let i = 0; i < superclass.childCount; i++) { + const child = superclass.child(i); + if ( + child && + (child.type === 'type_identifier' || + child.type === 'identifier' || + child.type === 'generic_type') + ) { + const superName = child.type === 'generic_type' ? child.child(0)?.text : child.text; + if (superName) + ctx.classes.push({ name: nameNode.text, - kind: 'interface', + extends: superName, line: node.startPosition.row + 1, - endLine: nodeEndLine(node), }); - const body = node.childForFieldName('body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const child = body.child(i); - if (child && child.type === 'method_declaration') { - const methName = child.childForFieldName('name'); - if (methName) { - definitions.push({ - name: `${nameNode.text}.${methName.text}`, - kind: 'method', - line: child.startPosition.row + 1, - endLine: child.endPosition.row + 1, - }); - } - } - } - } - } break; } + } + } - case 'enum_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const enumChildren = extractEnumConstants(node); - definitions.push({ - name: nameNode.text, - kind: 'enum', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: enumChildren.length > 0 ? enumChildren : undefined, - }); - } - break; - } + const interfaces = node.childForFieldName('interfaces'); + if (interfaces) { + extractJavaInterfaces(interfaces, nameNode.text, node.startPosition.row + 1, ctx); + } +} - case 'method_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findJavaParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const params = extractJavaParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: extractModifierVisibility(node), - }); +function extractJavaInterfaces(interfaces, className, line, ctx) { + for (let i = 0; i < interfaces.childCount; i++) { + const child = interfaces.child(i); + if ( + child && + (child.type === 'type_identifier' || + child.type === 'identifier' || + child.type === 'type_list' || + child.type === 'generic_type') + ) { + if (child.type === 'type_list') { + for (let j = 0; j < child.childCount; j++) { + const t = child.child(j); + if ( + t && + (t.type === 'type_identifier' || t.type === 'identifier' || t.type === 'generic_type') + ) { + const ifaceName = t.type === 'generic_type' ? t.child(0)?.text : t.text; + if (ifaceName) ctx.classes.push({ name: className, implements: ifaceName, line }); + } } - break; + } else { + const ifaceName = child.type === 'generic_type' ? child.child(0)?.text : child.text; + if (ifaceName) ctx.classes.push({ name: className, implements: ifaceName, line }); } + } + } +} - case 'constructor_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findJavaParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const params = extractJavaParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, +function handleJavaInterfaceDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + const body = node.childForFieldName('body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'method_declaration') { + const methName = child.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: extractModifierVisibility(node), + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, }); } - break; } + } + } +} - case 'import_declaration': { - 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(); - imports.push({ - source: fullPath, - names: [lastName], - line: node.startPosition.row + 1, - javaImport: true, - }); - } - if (child && child.type === 'asterisk') { - const lastImport = imports[imports.length - 1]; - if (lastImport) lastImport.names = ['*']; - } - } - break; - } +function handleJavaEnumDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const enumChildren = extractEnumConstants(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: enumChildren.length > 0 ? enumChildren : undefined, + }); +} - case 'method_invocation': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const obj = node.childForFieldName('object'); - const call = { name: nameNode.text, line: node.startPosition.row + 1 }; - if (obj) call.receiver = obj.text; - calls.push(call); - } - break; - } +function handleJavaMethodDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentClass = findJavaParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const params = extractJavaParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} - case 'object_creation_expression': { - const typeNode = node.childForFieldName('type'); - if (typeNode) { - const typeName = - typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text; - if (typeName) calls.push({ name: typeName, line: node.startPosition.row + 1 }); - } - break; - } - } +function handleJavaConstructorDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentClass = findJavaParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const params = extractJavaParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} - for (let i = 0; i < node.childCount; i++) walkJavaNode(node.child(i)); +function handleJavaImportDecl(node, ctx) { + 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(); + ctx.imports.push({ + source: fullPath, + names: [lastName], + line: node.startPosition.row + 1, + javaImport: true, + }); + } + if (child && child.type === 'asterisk') { + const lastImport = ctx.imports[ctx.imports.length - 1]; + if (lastImport) lastImport.names = ['*']; + } } +} + +function handleJavaMethodInvocation(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const obj = node.childForFieldName('object'); + const call = { name: nameNode.text, line: node.startPosition.row + 1 }; + if (obj) call.receiver = obj.text; + ctx.calls.push(call); +} - walkJavaNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function handleJavaObjectCreation(node, ctx) { + 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) { + let current = node.parent; + while (current) { + if ( + current.type === 'class_declaration' || + current.type === 'enum_declaration' || + current.type === 'interface_declaration' + ) { + const nameNode = current.childForFieldName('name'); + return nameNode ? nameNode.text : null; + } + current = current.parent; + } + return null; } // ── Child extraction helpers ──────────────────────────────────────────────── From bf5b986f3407dcbf9c6734e47e28aec39229e406 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:47:27 -0600 Subject: [PATCH 10/14] refactor: decompose remaining language extractors Apply the same per-category handler decomposition to all remaining language extractors: Go (6 handlers), Ruby (8 handlers), PHP (11 handlers), C# (11 handlers), Rust (9 handlers), HCL (4 handlers). Each extractor now follows the template established by the JS extractor: - Thin entry function creates ctx, delegates to walkXNode - walkXNode is a thin dispatcher switch - Each case is a named handler function at module scope - Helper functions (findParentClass, etc.) moved to module scope Impact: 66 functions changed, 23 affected --- src/extractors/csharp.js | 429 ++++++++++++++++++------------------ src/extractors/go.js | 349 +++++++++++++++--------------- src/extractors/hcl.js | 172 ++++++++------- src/extractors/php.js | 453 ++++++++++++++++++++------------------- src/extractors/ruby.js | 377 ++++++++++++++++---------------- src/extractors/rust.js | 347 +++++++++++++++--------------- 6 files changed, 1097 insertions(+), 1030 deletions(-) diff --git a/src/extractors/csharp.js b/src/extractors/csharp.js index 9dafa451..d52aa893 100644 --- a/src/extractors/csharp.js +++ b/src/extractors/csharp.js @@ -4,233 +4,248 @@ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js' * Extract symbols from C# files. */ export function extractCSharpSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function findCSharpParentType(node) { - let current = node.parent; - while (current) { - if ( - current.type === 'class_declaration' || - current.type === 'struct_declaration' || - current.type === 'interface_declaration' || - current.type === 'enum_declaration' || - current.type === 'record_declaration' - ) { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - current = current.parent; - } - return null; + walkCSharpNode(tree.rootNode, ctx); + return ctx; +} + +function walkCSharpNode(node, ctx) { + switch (node.type) { + case 'class_declaration': + handleCsClassDecl(node, ctx); + break; + case 'struct_declaration': + handleCsStructDecl(node, ctx); + break; + case 'record_declaration': + handleCsRecordDecl(node, ctx); + break; + case 'interface_declaration': + handleCsInterfaceDecl(node, ctx); + break; + case 'enum_declaration': + handleCsEnumDecl(node, ctx); + break; + case 'method_declaration': + handleCsMethodDecl(node, ctx); + break; + case 'constructor_declaration': + handleCsConstructorDecl(node, ctx); + break; + case 'property_declaration': + handleCsPropertyDecl(node, ctx); + break; + case 'using_directive': + handleCsUsingDirective(node, ctx); + break; + case 'invocation_expression': + handleCsInvocationExpr(node, ctx); + break; + case 'object_creation_expression': + handleCsObjectCreation(node, ctx); + break; } - function walkCSharpNode(node) { - switch (node.type) { - case 'class_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const classChildren = extractCSharpClassFields(node); - definitions.push({ - name: nameNode.text, - kind: 'class', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: classChildren.length > 0 ? classChildren : undefined, - }); - extractCSharpBaseTypes(node, nameNode.text, classes); - } - break; - } + for (let i = 0; i < node.childCount; i++) walkCSharpNode(node.child(i), ctx); +} - case 'struct_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const structChildren = extractCSharpClassFields(node); - definitions.push({ - name: nameNode.text, - kind: 'struct', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: structChildren.length > 0 ? structChildren : undefined, - }); - extractCSharpBaseTypes(node, nameNode.text, classes); - } - break; - } +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - case 'record_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'record', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - extractCSharpBaseTypes(node, nameNode.text, classes); - } - break; - } +function handleCsClassDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const classChildren = extractCSharpClassFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: classChildren.length > 0 ? classChildren : undefined, + }); + extractCSharpBaseTypes(node, nameNode.text, ctx.classes); +} - case 'interface_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'interface', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - const body = node.childForFieldName('body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const child = body.child(i); - if (child && child.type === 'method_declaration') { - const methName = child.childForFieldName('name'); - if (methName) { - definitions.push({ - name: `${nameNode.text}.${methName.text}`, - kind: 'method', - line: child.startPosition.row + 1, - endLine: child.endPosition.row + 1, - }); - } - } - } - } - } - break; - } +function handleCsStructDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const structChildren = extractCSharpClassFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'struct', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: structChildren.length > 0 ? structChildren : undefined, + }); + extractCSharpBaseTypes(node, nameNode.text, ctx.classes); +} - case 'enum_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const enumChildren = extractCSharpEnumMembers(node); - definitions.push({ - name: nameNode.text, - kind: 'enum', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: enumChildren.length > 0 ? enumChildren : undefined, - }); - } - break; - } +function handleCsRecordDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'record', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + extractCSharpBaseTypes(node, nameNode.text, ctx.classes); +} - case 'method_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentType = findCSharpParentType(node); - const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; - const params = extractCSharpParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, +function handleCsInterfaceDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + const body = node.childForFieldName('body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'method_declaration') { + const methName = child.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: extractModifierVisibility(node), + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, }); } - break; } + } + } +} - case 'constructor_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentType = findCSharpParentType(node); - const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; - const params = extractCSharpParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: extractModifierVisibility(node), - }); - } - break; - } +function handleCsEnumDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const enumChildren = extractCSharpEnumMembers(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: enumChildren.length > 0 ? enumChildren : undefined, + }); +} - case 'property_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentType = findCSharpParentType(node); - const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; - definitions.push({ - name: fullName, - kind: 'property', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - visibility: extractModifierVisibility(node), - }); - } - break; - } +function handleCsMethodDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentType = findCSharpParentType(node); + const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; + const params = extractCSharpParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} - case 'using_directive': { - // using System.Collections.Generic; - const nameNode = - node.childForFieldName('name') || - findChild(node, 'qualified_name') || - findChild(node, 'identifier'); - if (nameNode) { - const fullPath = nameNode.text; - const lastName = fullPath.split('.').pop(); - imports.push({ - source: fullPath, - names: [lastName], - line: node.startPosition.row + 1, - csharpUsing: true, - }); - } - break; - } +function handleCsConstructorDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentType = findCSharpParentType(node); + const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; + const params = extractCSharpParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} - case 'invocation_expression': { - const fn = node.childForFieldName('function') || node.child(0); - if (fn) { - if (fn.type === 'identifier') { - calls.push({ name: fn.text, line: node.startPosition.row + 1 }); - } else if (fn.type === 'member_access_expression') { - const name = fn.childForFieldName('name'); - if (name) { - const expr = fn.childForFieldName('expression'); - const call = { name: name.text, line: node.startPosition.row + 1 }; - if (expr) call.receiver = expr.text; - calls.push(call); - } - } else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') { - const name = fn.childForFieldName('name') || fn.child(0); - if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 }); - } - } - break; - } +function handleCsPropertyDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentType = findCSharpParentType(node); + const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text; + ctx.definitions.push({ + name: fullName, + kind: 'property', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + visibility: extractModifierVisibility(node), + }); +} - case 'object_creation_expression': { - const typeNode = node.childForFieldName('type'); - if (typeNode) { - const typeName = - typeNode.type === 'generic_name' - ? typeNode.childForFieldName('name')?.text || typeNode.child(0)?.text - : typeNode.text; - if (typeName) calls.push({ name: typeName, line: node.startPosition.row + 1 }); - } - break; - } - } +function handleCsUsingDirective(node, ctx) { + const nameNode = + node.childForFieldName('name') || + findChild(node, 'qualified_name') || + findChild(node, 'identifier'); + if (!nameNode) return; + const fullPath = nameNode.text; + const lastName = fullPath.split('.').pop(); + ctx.imports.push({ + source: fullPath, + names: [lastName], + line: node.startPosition.row + 1, + csharpUsing: true, + }); +} - for (let i = 0; i < node.childCount; i++) walkCSharpNode(node.child(i)); +function handleCsInvocationExpr(node, ctx) { + const fn = node.childForFieldName('function') || node.child(0); + if (!fn) return; + if (fn.type === 'identifier') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'member_access_expression') { + const name = fn.childForFieldName('name'); + if (name) { + const expr = fn.childForFieldName('expression'); + const call = { name: name.text, line: node.startPosition.row + 1 }; + if (expr) call.receiver = expr.text; + ctx.calls.push(call); + } + } else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') { + const name = fn.childForFieldName('name') || fn.child(0); + if (name) ctx.calls.push({ name: name.text, line: node.startPosition.row + 1 }); } +} + +function handleCsObjectCreation(node, ctx) { + const typeNode = node.childForFieldName('type'); + if (!typeNode) return; + const typeName = + typeNode.type === 'generic_name' + ? typeNode.childForFieldName('name')?.text || typeNode.child(0)?.text + : typeNode.text; + if (typeName) ctx.calls.push({ name: typeName, line: node.startPosition.row + 1 }); +} - walkCSharpNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function findCSharpParentType(node) { + let current = node.parent; + while (current) { + if ( + current.type === 'class_declaration' || + current.type === 'struct_declaration' || + current.type === 'interface_declaration' || + current.type === 'enum_declaration' || + current.type === 'record_declaration' + ) { + const nameNode = current.childForFieldName('name'); + return nameNode ? nameNode.text : null; + } + current = current.parent; + } + return null; } // ── Child extraction helpers ──────────────────────────────────────────────── diff --git a/src/extractors/go.js b/src/extractors/go.js index 50460c8d..57d3b2a8 100644 --- a/src/extractors/go.js +++ b/src/extractors/go.js @@ -4,196 +4,201 @@ import { findChild, goVisibility, nodeEndLine } from './helpers.js'; * Extract symbols from Go files. */ export function extractGoSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function walkGoNode(node) { - switch (node.type) { - case 'function_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const params = extractGoParameters(node.childForFieldName('parameters')); - definitions.push({ - name: nameNode.text, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: goVisibility(nameNode.text), - }); - } - break; - } + walkGoNode(tree.rootNode, ctx); + return ctx; +} - case 'method_declaration': { - const nameNode = node.childForFieldName('name'); - const receiver = node.childForFieldName('receiver'); - if (nameNode) { - let receiverType = null; - if (receiver) { - // receiver is a parameter_list like (r *Foo) or (r Foo) - for (let i = 0; i < receiver.childCount; i++) { - const param = receiver.child(i); - if (!param) continue; - const typeNode = param.childForFieldName('type'); - if (typeNode) { - receiverType = - typeNode.type === 'pointer_type' - ? typeNode.text.replace(/^\*/, '') - : typeNode.text; - break; - } - } - } - const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text; - const params = extractGoParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: goVisibility(nameNode.text), - }); - } - break; - } +function walkGoNode(node, ctx) { + switch (node.type) { + case 'function_declaration': + handleGoFuncDecl(node, ctx); + break; + case 'method_declaration': + handleGoMethodDecl(node, ctx); + break; + case 'type_declaration': + handleGoTypeDecl(node, ctx); + break; + case 'import_declaration': + handleGoImportDecl(node, ctx); + break; + case 'const_declaration': + handleGoConstDecl(node, ctx); + break; + case 'call_expression': + handleGoCallExpr(node, ctx); + break; + } - case 'type_declaration': { - for (let i = 0; i < node.childCount; i++) { - const spec = node.child(i); - if (!spec || spec.type !== 'type_spec') continue; - const nameNode = spec.childForFieldName('name'); - const typeNode = spec.childForFieldName('type'); - if (nameNode && typeNode) { - if (typeNode.type === 'struct_type') { - const fields = extractStructFields(typeNode); - definitions.push({ - name: nameNode.text, - kind: 'struct', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: fields.length > 0 ? fields : undefined, - }); - } else if (typeNode.type === 'interface_type') { - definitions.push({ - name: nameNode.text, - kind: 'interface', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - for (let j = 0; j < typeNode.childCount; j++) { - const member = typeNode.child(j); - if (member && member.type === 'method_elem') { - const methName = member.childForFieldName('name'); - if (methName) { - definitions.push({ - name: `${nameNode.text}.${methName.text}`, - kind: 'method', - line: member.startPosition.row + 1, - endLine: member.endPosition.row + 1, - }); - } - } - } - } else { - definitions.push({ - name: nameNode.text, - kind: 'type', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - } - } - } + for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i), ctx); +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleGoFuncDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (nameNode) { + const params = extractGoParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: goVisibility(nameNode.text), + }); + } +} + +function handleGoMethodDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + const receiver = node.childForFieldName('receiver'); + if (!nameNode) return; + let receiverType = null; + if (receiver) { + for (let i = 0; i < receiver.childCount; i++) { + const param = receiver.child(i); + if (!param) continue; + const typeNode = param.childForFieldName('type'); + if (typeNode) { + receiverType = + typeNode.type === 'pointer_type' ? typeNode.text.replace(/^\*/, '') : typeNode.text; break; } + } + } + const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text; + const params = extractGoParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: goVisibility(nameNode.text), + }); +} - case 'import_declaration': { - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (!child) continue; - if (child.type === 'import_spec') { - const pathNode = child.childForFieldName('path'); - if (pathNode) { - const importPath = pathNode.text.replace(/"/g, ''); - const nameNode = child.childForFieldName('name'); - const alias = nameNode ? nameNode.text : importPath.split('/').pop(); - imports.push({ - source: importPath, - names: [alias], - line: child.startPosition.row + 1, - goImport: true, +function handleGoTypeDecl(node, ctx) { + for (let i = 0; i < node.childCount; i++) { + const spec = node.child(i); + if (!spec || spec.type !== 'type_spec') continue; + const nameNode = spec.childForFieldName('name'); + const typeNode = spec.childForFieldName('type'); + if (nameNode && typeNode) { + if (typeNode.type === 'struct_type') { + const fields = extractStructFields(typeNode); + ctx.definitions.push({ + name: nameNode.text, + kind: 'struct', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: fields.length > 0 ? fields : undefined, + }); + } else if (typeNode.type === 'interface_type') { + ctx.definitions.push({ + name: nameNode.text, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + for (let j = 0; j < typeNode.childCount; j++) { + const member = typeNode.child(j); + if (member && member.type === 'method_elem') { + const methName = member.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, + kind: 'method', + line: member.startPosition.row + 1, + endLine: member.endPosition.row + 1, }); } } - if (child.type === 'import_spec_list') { - for (let j = 0; j < child.childCount; j++) { - const spec = child.child(j); - if (spec && spec.type === 'import_spec') { - 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(); - imports.push({ - source: importPath, - names: [alias], - line: spec.startPosition.row + 1, - goImport: true, - }); - } - } - } - } - } - break; - } - - case 'const_declaration': { - for (let i = 0; i < node.childCount; i++) { - const spec = node.child(i); - if (!spec || spec.type !== 'const_spec') continue; - const constName = spec.childForFieldName('name'); - if (constName) { - definitions.push({ - name: constName.text, - kind: 'constant', - line: spec.startPosition.row + 1, - endLine: spec.endPosition.row + 1, - }); - } } - break; + } else { + ctx.definitions.push({ + name: nameNode.text, + kind: 'type', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); } + } + } +} - case 'call_expression': { - const fn = node.childForFieldName('function'); - if (fn) { - if (fn.type === 'identifier') { - calls.push({ name: fn.text, line: node.startPosition.row + 1 }); - } else if (fn.type === 'selector_expression') { - const field = fn.childForFieldName('field'); - if (field) { - const operand = fn.childForFieldName('operand'); - const call = { name: field.text, line: node.startPosition.row + 1 }; - if (operand) call.receiver = operand.text; - calls.push(call); - } - } +function handleGoImportDecl(node, ctx) { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (!child) continue; + if (child.type === 'import_spec') { + extractGoImportSpec(child, ctx); + } + if (child.type === 'import_spec_list') { + for (let j = 0; j < child.childCount; j++) { + const spec = child.child(j); + if (spec && spec.type === 'import_spec') { + extractGoImportSpec(spec, ctx); } - break; } } + } +} + +function extractGoImportSpec(spec, ctx) { + 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(); + ctx.imports.push({ + source: importPath, + names: [alias], + line: spec.startPosition.row + 1, + goImport: true, + }); + } +} - for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i)); +function handleGoConstDecl(node, ctx) { + for (let i = 0; i < node.childCount; i++) { + const spec = node.child(i); + if (!spec || spec.type !== 'const_spec') continue; + const constName = spec.childForFieldName('name'); + if (constName) { + ctx.definitions.push({ + name: constName.text, + kind: 'constant', + line: spec.startPosition.row + 1, + endLine: spec.endPosition.row + 1, + }); + } } +} - walkGoNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function handleGoCallExpr(node, ctx) { + const fn = node.childForFieldName('function'); + if (!fn) return; + if (fn.type === 'identifier') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'selector_expression') { + const field = fn.childForFieldName('field'); + if (field) { + const operand = fn.childForFieldName('operand'); + const call = { name: field.text, line: node.startPosition.row + 1 }; + if (operand) call.receiver = operand.text; + ctx.calls.push(call); + } + } } // ── Child extraction helpers ──────────────────────────────────────────────── diff --git a/src/extractors/hcl.js b/src/extractors/hcl.js index aba022a5..8b13651f 100644 --- a/src/extractors/hcl.js +++ b/src/extractors/hcl.js @@ -4,92 +4,108 @@ import { nodeEndLine } from './helpers.js'; * Extract symbols from HCL (Terraform) files. */ export function extractHCLSymbols(tree, _filePath) { - const definitions = []; - const imports = []; + const ctx = { definitions: [], imports: [] }; - function walkHclNode(node) { - if (node.type === 'block') { - const children = []; - for (let i = 0; i < node.childCount; i++) children.push(node.child(i)); + walkHclNode(tree.rootNode, ctx); + return { + definitions: ctx.definitions, + calls: [], + imports: ctx.imports, + classes: [], + exports: [], + }; +} - const identifiers = children.filter((c) => c.type === 'identifier'); - const strings = children.filter((c) => c.type === 'string_lit'); +function walkHclNode(node, ctx) { + if (node.type === 'block') { + handleHclBlock(node, ctx); + } - if (identifiers.length > 0) { - const blockType = identifiers[0].text; - let name = ''; + for (let i = 0; i < node.childCount; i++) walkHclNode(node.child(i), ctx); +} - if (blockType === 'resource' && strings.length >= 2) { - name = `${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; - } else if (blockType === 'data' && strings.length >= 2) { - name = `data.${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; - } else if ( - (blockType === 'variable' || blockType === 'output' || blockType === 'module') && - strings.length >= 1 - ) { - name = `${blockType}.${strings[0].text.replace(/"/g, '')}`; - } else if (blockType === 'locals') { - name = 'locals'; - } else if (blockType === 'terraform' || blockType === 'provider') { - name = blockType; - if (strings.length >= 1) name += `.${strings[0].text.replace(/"/g, '')}`; - } +function handleHclBlock(node, ctx) { + const children = []; + for (let i = 0; i < node.childCount; i++) children.push(node.child(i)); - if (name) { - // Extract attributes as property children for variable/output blocks - let blockChildren; - if (blockType === 'variable' || blockType === 'output') { - blockChildren = []; - const body = children.find((c) => c.type === 'body'); - if (body) { - for (let j = 0; j < body.childCount; j++) { - const attr = body.child(j); - if (attr && attr.type === 'attribute') { - const key = attr.childForFieldName('key') || attr.child(0); - if (key) { - blockChildren.push({ - name: key.text, - kind: 'property', - line: attr.startPosition.row + 1, - }); - } - } - } - } - } - definitions.push({ - name, - kind: blockType, - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: blockChildren?.length > 0 ? blockChildren : undefined, - }); - } + const identifiers = children.filter((c) => c.type === 'identifier'); + const strings = children.filter((c) => c.type === 'string_lit'); - if (blockType === 'module') { - const body = children.find((c) => c.type === 'body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const attr = body.child(i); - if (attr && attr.type === 'attribute') { - const key = attr.childForFieldName('key') || attr.child(0); - const val = attr.childForFieldName('val') || attr.child(2); - if (key && key.text === 'source' && val) { - const src = val.text.replace(/"/g, ''); - if (src.startsWith('./') || src.startsWith('../')) { - imports.push({ source: src, names: [], line: attr.startPosition.row + 1 }); - } - } - } - } - } - } - } + if (identifiers.length === 0) return; + const blockType = identifiers[0].text; + const name = resolveHclBlockName(blockType, strings); + + if (name) { + let blockChildren; + if (blockType === 'variable' || blockType === 'output') { + blockChildren = extractHclAttributes(children); } + ctx.definitions.push({ + name, + kind: blockType, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: blockChildren?.length > 0 ? blockChildren : undefined, + }); + } - for (let i = 0; i < node.childCount; i++) walkHclNode(node.child(i)); + if (blockType === 'module') { + extractHclModuleSource(children, node, ctx); } +} - walkHclNode(tree.rootNode); - return { definitions, calls: [], imports, classes: [], exports: [] }; +function resolveHclBlockName(blockType, strings) { + if (blockType === 'resource' && strings.length >= 2) { + return `${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; + } + if (blockType === 'data' && strings.length >= 2) { + return `data.${strings[0].text.replace(/"/g, '')}.${strings[1].text.replace(/"/g, '')}`; + } + if ( + (blockType === 'variable' || blockType === 'output' || blockType === 'module') && + strings.length >= 1 + ) { + return `${blockType}.${strings[0].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, '')}`; + return name; + } + return ''; +} + +function extractHclAttributes(children) { + const attrs = []; + const body = children.find((c) => c.type === 'body'); + if (!body) return attrs; + for (let j = 0; j < body.childCount; j++) { + const attr = body.child(j); + if (attr && attr.type === 'attribute') { + const key = attr.childForFieldName('key') || attr.child(0); + if (key) { + attrs.push({ name: key.text, kind: 'property', line: attr.startPosition.row + 1 }); + } + } + } + return attrs; +} + +function extractHclModuleSource(children, _node, ctx) { + const body = children.find((c) => c.type === 'body'); + if (!body) return; + for (let i = 0; i < body.childCount; i++) { + const attr = body.child(i); + if (attr && attr.type === 'attribute') { + const key = attr.childForFieldName('key') || attr.child(0); + const val = attr.childForFieldName('val') || attr.child(2); + if (key && key.text === 'source' && val) { + const src = val.text.replace(/"/g, ''); + if (src.startsWith('./') || src.startsWith('../')) { + ctx.imports.push({ source: src, names: [], line: attr.startPosition.row + 1 }); + } + } + } + } } diff --git a/src/extractors/php.js b/src/extractors/php.js index fd008168..03f9c6d7 100644 --- a/src/extractors/php.js +++ b/src/extractors/php.js @@ -76,249 +76,260 @@ function extractPhpEnumCases(enumNode) { * Extract symbols from PHP files. */ export function extractPHPSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function findPHPParentClass(node) { - let current = node.parent; - while (current) { - if ( - current.type === 'class_declaration' || - current.type === 'trait_declaration' || - current.type === 'enum_declaration' - ) { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - current = current.parent; - } - return null; + walkPhpNode(tree.rootNode, ctx); + return ctx; +} + +function walkPhpNode(node, ctx) { + switch (node.type) { + case 'function_definition': + handlePhpFuncDef(node, ctx); + break; + case 'class_declaration': + handlePhpClassDecl(node, ctx); + break; + case 'interface_declaration': + handlePhpInterfaceDecl(node, ctx); + break; + case 'trait_declaration': + handlePhpTraitDecl(node, ctx); + break; + case 'enum_declaration': + handlePhpEnumDecl(node, ctx); + break; + case 'method_declaration': + handlePhpMethodDecl(node, ctx); + break; + case 'namespace_use_declaration': + handlePhpNamespaceUse(node, ctx); + break; + case 'function_call_expression': + handlePhpFuncCall(node, ctx); + break; + case 'member_call_expression': + handlePhpMemberCall(node, ctx); + break; + case 'scoped_call_expression': + handlePhpScopedCall(node, ctx); + break; + case 'object_creation_expression': + handlePhpObjectCreation(node, ctx); + break; } - function walkPhpNode(node) { - switch (node.type) { - case 'function_definition': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const params = extractPhpParameters(node); - definitions.push({ - name: nameNode.text, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - }); - } - break; - } + for (let i = 0; i < node.childCount; i++) walkPhpNode(node.child(i), ctx); +} - case 'class_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const classChildren = extractPhpClassChildren(node); - definitions.push({ - name: nameNode.text, - kind: 'class', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: classChildren.length > 0 ? classChildren : undefined, - }); +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - // Check base clause (extends) - const baseClause = - node.childForFieldName('base_clause') || findChild(node, 'base_clause'); - if (baseClause) { - for (let i = 0; i < baseClause.childCount; i++) { - const child = baseClause.child(i); - if (child && (child.type === 'name' || child.type === 'qualified_name')) { - classes.push({ - name: nameNode.text, - extends: child.text, - line: node.startPosition.row + 1, - }); - break; - } - } - } +function handlePhpFuncDef(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const params = extractPhpParameters(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + }); +} - // Check class interface clause (implements) - const interfaceClause = findChild(node, 'class_interface_clause'); - if (interfaceClause) { - for (let i = 0; i < interfaceClause.childCount; i++) { - const child = interfaceClause.child(i); - if (child && (child.type === 'name' || child.type === 'qualified_name')) { - classes.push({ - name: nameNode.text, - implements: child.text, - line: node.startPosition.row + 1, - }); - } - } - } - } +function handlePhpClassDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const classChildren = extractPhpClassChildren(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: classChildren.length > 0 ? classChildren : undefined, + }); + const baseClause = node.childForFieldName('base_clause') || findChild(node, 'base_clause'); + if (baseClause) { + for (let i = 0; i < baseClause.childCount; i++) { + const child = baseClause.child(i); + if (child && (child.type === 'name' || child.type === 'qualified_name')) { + ctx.classes.push({ + name: nameNode.text, + extends: child.text, + line: node.startPosition.row + 1, + }); break; } - - case 'interface_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'interface', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - const body = node.childForFieldName('body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const child = body.child(i); - if (child && child.type === 'method_declaration') { - const methName = child.childForFieldName('name'); - if (methName) { - definitions.push({ - name: `${nameNode.text}.${methName.text}`, - kind: 'method', - line: child.startPosition.row + 1, - endLine: child.endPosition.row + 1, - }); - } - } - } - } - } - break; + } + } + const interfaceClause = findChild(node, 'class_interface_clause'); + if (interfaceClause) { + for (let i = 0; i < interfaceClause.childCount; i++) { + const child = interfaceClause.child(i); + if (child && (child.type === 'name' || child.type === 'qualified_name')) { + ctx.classes.push({ + name: nameNode.text, + implements: child.text, + line: node.startPosition.row + 1, + }); } + } + } +} - case 'trait_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'trait', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), +function handlePhpInterfaceDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'interface', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + const body = node.childForFieldName('body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && child.type === 'method_declaration') { + const methName = child.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, }); } - break; } + } + } +} - case 'enum_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const enumChildren = extractPhpEnumCases(node); - definitions.push({ - name: nameNode.text, - kind: 'enum', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: enumChildren.length > 0 ? enumChildren : undefined, - }); - } - break; - } +function handlePhpTraitDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'trait', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} - case 'method_declaration': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findPHPParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const params = extractPhpParameters(node); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: extractModifierVisibility(node), - }); - } - break; - } +function handlePhpEnumDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const enumChildren = extractPhpEnumCases(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: enumChildren.length > 0 ? enumChildren : undefined, + }); +} - case 'namespace_use_declaration': { - // use App\Models\User; - 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 alias = child.childForFieldName('alias'); - imports.push({ - source: fullPath, - names: [alias ? alias.text : lastName], - line: node.startPosition.row + 1, - phpUse: true, - }); - } - } - // Single use clause without wrapper - if (child && (child.type === 'qualified_name' || child.type === 'name')) { - const fullPath = child.text; - const lastName = fullPath.split('\\').pop(); - imports.push({ - source: fullPath, - names: [lastName], - line: node.startPosition.row + 1, - phpUse: true, - }); - } - } - break; - } +function handlePhpMethodDecl(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentClass = findPHPParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const params = extractPhpParameters(node); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: extractModifierVisibility(node), + }); +} - case 'function_call_expression': { - const fn = node.childForFieldName('function') || node.child(0); - if (fn) { - if (fn.type === 'name' || fn.type === 'identifier') { - calls.push({ name: fn.text, line: node.startPosition.row + 1 }); - } else if (fn.type === 'qualified_name') { - const parts = fn.text.split('\\'); - calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 }); - } - } - break; +function handlePhpNamespaceUse(node, ctx) { + 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 alias = child.childForFieldName('alias'); + ctx.imports.push({ + source: fullPath, + names: [alias ? alias.text : lastName], + line: node.startPosition.row + 1, + phpUse: true, + }); } + } + if (child && (child.type === 'qualified_name' || child.type === 'name')) { + const fullPath = child.text; + const lastName = fullPath.split('\\').pop(); + ctx.imports.push({ + source: fullPath, + names: [lastName], + line: node.startPosition.row + 1, + phpUse: true, + }); + } + } +} - case 'member_call_expression': { - const name = node.childForFieldName('name'); - if (name) { - const obj = node.childForFieldName('object'); - const call = { name: name.text, line: node.startPosition.row + 1 }; - if (obj) call.receiver = obj.text; - calls.push(call); - } - break; - } +function handlePhpFuncCall(node, ctx) { + 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 }); + } +} - case 'scoped_call_expression': { - const name = node.childForFieldName('name'); - if (name) { - const scope = node.childForFieldName('scope'); - const call = { name: name.text, line: node.startPosition.row + 1 }; - if (scope) call.receiver = scope.text; - calls.push(call); - } - break; - } +function handlePhpMemberCall(node, ctx) { + const name = node.childForFieldName('name'); + if (!name) return; + const obj = node.childForFieldName('object'); + const call = { name: name.text, line: node.startPosition.row + 1 }; + if (obj) call.receiver = obj.text; + ctx.calls.push(call); +} - case 'object_creation_expression': { - const classNode = node.child(1); // skip 'new' keyword - if (classNode && (classNode.type === 'name' || classNode.type === 'qualified_name')) { - const parts = classNode.text.split('\\'); - calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 }); - } - break; - } - } +function handlePhpScopedCall(node, ctx) { + const name = node.childForFieldName('name'); + if (!name) return; + const scope = node.childForFieldName('scope'); + const call = { name: name.text, line: node.startPosition.row + 1 }; + if (scope) call.receiver = scope.text; + ctx.calls.push(call); +} - for (let i = 0; i < node.childCount; i++) walkPhpNode(node.child(i)); +function handlePhpObjectCreation(node, ctx) { + 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 }); } +} - walkPhpNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function findPHPParentClass(node) { + let current = node.parent; + while (current) { + if ( + current.type === 'class_declaration' || + current.type === 'trait_declaration' || + current.type === 'enum_declaration' + ) { + const nameNode = current.childForFieldName('name'); + return nameNode ? nameNode.text : null; + } + current = current.parent; + } + return null; } diff --git a/src/extractors/ruby.js b/src/extractors/ruby.js index 400d410d..cc0da5fd 100644 --- a/src/extractors/ruby.js +++ b/src/extractors/ruby.js @@ -4,211 +4,218 @@ import { findChild, nodeEndLine } from './helpers.js'; * Extract symbols from Ruby files. */ export function extractRubySymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function findRubyParentClass(node) { - let current = node.parent; - while (current) { - if (current.type === 'class') { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - if (current.type === 'module') { - const nameNode = current.childForFieldName('name'); - return nameNode ? nameNode.text : null; - } - current = current.parent; - } - return null; + walkRubyNode(tree.rootNode, ctx); + return ctx; +} + +function walkRubyNode(node, ctx) { + switch (node.type) { + case 'class': + handleRubyClass(node, ctx); + break; + case 'module': + handleRubyModule(node, ctx); + break; + case 'method': + handleRubyMethod(node, ctx); + break; + case 'singleton_method': + handleRubySingletonMethod(node, ctx); + break; + case 'assignment': + handleRubyAssignment(node, ctx); + break; + case 'call': + handleRubyCall(node, ctx); + break; } - function walkRubyNode(node) { - switch (node.type) { - case 'class': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const classChildren = extractRubyClassChildren(node); - definitions.push({ - name: nameNode.text, - kind: 'class', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: classChildren.length > 0 ? classChildren : undefined, - }); - const superclass = node.childForFieldName('superclass'); - if (superclass) { - // superclass wraps the < token and class name - for (let i = 0; i < superclass.childCount; i++) { - const child = superclass.child(i); - if (child && (child.type === 'constant' || child.type === 'scope_resolution')) { - classes.push({ - name: nameNode.text, - extends: child.text, - line: node.startPosition.row + 1, - }); - break; - } - } - // Direct superclass node may be a constant - if (superclass.type === 'superclass') { - for (let i = 0; i < superclass.childCount; i++) { - const child = superclass.child(i); - if (child && (child.type === 'constant' || child.type === 'scope_resolution')) { - classes.push({ - name: nameNode.text, - extends: child.text, - line: node.startPosition.row + 1, - }); - break; - } - } - } - } - } + for (let i = 0; i < node.childCount; i++) walkRubyNode(node.child(i), ctx); +} + +// ── Walk-path per-node-type handlers ──────────────────────────────────────── + +function handleRubyClass(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const classChildren = extractRubyClassChildren(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'class', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: classChildren.length > 0 ? classChildren : undefined, + }); + const superclass = node.childForFieldName('superclass'); + if (superclass) { + for (let i = 0; i < superclass.childCount; i++) { + const child = superclass.child(i); + if (child && (child.type === 'constant' || child.type === 'scope_resolution')) { + ctx.classes.push({ + name: nameNode.text, + extends: child.text, + line: node.startPosition.row + 1, + }); break; } - - case 'module': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const moduleChildren = extractRubyBodyConstants(node); - definitions.push({ + } + if (superclass.type === 'superclass') { + for (let i = 0; i < superclass.childCount; i++) { + const child = superclass.child(i); + if (child && (child.type === 'constant' || child.type === 'scope_resolution')) { + ctx.classes.push({ name: nameNode.text, - kind: 'module', + extends: child.text, line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: moduleChildren.length > 0 ? moduleChildren : undefined, }); + break; } - break; } + } + } +} - case 'method': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findRubyParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const params = extractRubyParameters(node); - definitions.push({ - name: fullName, - kind: 'method', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - }); - } - break; - } +function handleRubyModule(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const moduleChildren = extractRubyBodyConstants(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'module', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: moduleChildren.length > 0 ? moduleChildren : undefined, + }); +} - case 'singleton_method': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const parentClass = findRubyParentClass(node); - const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; - const params = extractRubyParameters(node); - definitions.push({ - name: fullName, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - }); - } - break; - } +function handleRubyMethod(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentClass = findRubyParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const params = extractRubyParameters(node); + ctx.definitions.push({ + name: fullName, + kind: 'method', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + }); +} - case 'assignment': { - // Top-level constant assignments (parent is program) - if (node.parent && node.parent.type === 'program') { - const left = node.childForFieldName('left'); - if (left && left.type === 'constant') { - definitions.push({ - name: left.text, - kind: 'constant', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - } - } - break; - } +function handleRubySingletonMethod(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const parentClass = findRubyParentClass(node); + const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text; + const params = extractRubyParameters(node); + ctx.definitions.push({ + name: fullName, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + }); +} - case 'call': { - const methodNode = node.childForFieldName('method'); - if (methodNode) { - // Check for require/require_relative - if (methodNode.text === 'require' || methodNode.text === 'require_relative') { - const args = node.childForFieldName('arguments'); - if (args) { - for (let i = 0; i < args.childCount; i++) { - const arg = args.child(i); - if (arg && (arg.type === 'string' || arg.type === 'string_content')) { - const strContent = arg.text.replace(/^['"]|['"]$/g, ''); - imports.push({ - source: strContent, - names: [strContent.split('/').pop()], - line: node.startPosition.row + 1, - rubyRequire: true, - }); - break; - } - // Look inside string for string_content - if (arg && arg.type === 'string') { - const content = findChild(arg, 'string_content'); - if (content) { - imports.push({ - source: content.text, - names: [content.text.split('/').pop()], - line: node.startPosition.row + 1, - rubyRequire: true, - }); - break; - } - } - } - } - } else if ( - methodNode.text === 'include' || - methodNode.text === 'extend' || - methodNode.text === 'prepend' - ) { - // Module inclusion — treated like implements - const parentClass = findRubyParentClass(node); - if (parentClass) { - const args = node.childForFieldName('arguments'); - if (args) { - for (let i = 0; i < args.childCount; i++) { - const arg = args.child(i); - if (arg && (arg.type === 'constant' || arg.type === 'scope_resolution')) { - classes.push({ - name: parentClass, - implements: arg.text, - line: node.startPosition.row + 1, - }); - } - } - } - } - } else { - const recv = node.childForFieldName('receiver'); - const call = { name: methodNode.text, line: node.startPosition.row + 1 }; - if (recv) call.receiver = recv.text; - calls.push(call); - } - } +function handleRubyAssignment(node, ctx) { + if (node.parent && node.parent.type === 'program') { + const left = node.childForFieldName('left'); + if (left && left.type === 'constant') { + ctx.definitions.push({ + name: left.text, + kind: 'constant', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + } + } +} + +function handleRubyCall(node, ctx) { + const methodNode = node.childForFieldName('method'); + if (!methodNode) return; + if (methodNode.text === 'require' || methodNode.text === 'require_relative') { + handleRubyRequire(node, ctx); + } else if ( + methodNode.text === 'include' || + methodNode.text === 'extend' || + methodNode.text === 'prepend' + ) { + handleRubyModuleInclusion(node, methodNode, ctx); + } else { + const recv = node.childForFieldName('receiver'); + const call = { name: methodNode.text, line: node.startPosition.row + 1 }; + if (recv) call.receiver = recv.text; + ctx.calls.push(call); + } +} + +function handleRubyRequire(node, ctx) { + const args = node.childForFieldName('arguments'); + if (!args) return; + for (let i = 0; i < args.childCount; i++) { + const arg = args.child(i); + if (arg && (arg.type === 'string' || arg.type === 'string_content')) { + const strContent = arg.text.replace(/^['"]|['"]$/g, ''); + ctx.imports.push({ + source: strContent, + names: [strContent.split('/').pop()], + line: node.startPosition.row + 1, + rubyRequire: true, + }); + break; + } + if (arg && arg.type === 'string') { + const content = findChild(arg, 'string_content'); + if (content) { + ctx.imports.push({ + source: content.text, + names: [content.text.split('/').pop()], + line: node.startPosition.row + 1, + rubyRequire: true, + }); break; } } + } +} - for (let i = 0; i < node.childCount; i++) walkRubyNode(node.child(i)); +function handleRubyModuleInclusion(node, _methodNode, ctx) { + const parentClass = findRubyParentClass(node); + if (!parentClass) return; + const args = node.childForFieldName('arguments'); + if (!args) return; + for (let i = 0; i < args.childCount; i++) { + const arg = args.child(i); + if (arg && (arg.type === 'constant' || arg.type === 'scope_resolution')) { + ctx.classes.push({ + name: parentClass, + implements: arg.text, + line: node.startPosition.row + 1, + }); + } } +} - walkRubyNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function findRubyParentClass(node) { + let current = node.parent; + while (current) { + if (current.type === 'class' || current.type === 'module') { + const nameNode = current.childForFieldName('name'); + return nameNode ? nameNode.text : null; + } + current = current.parent; + } + return null; } // ── Child extraction helpers ──────────────────────────────────────────────── diff --git a/src/extractors/rust.js b/src/extractors/rust.js index 705f9bd0..389bec00 100644 --- a/src/extractors/rust.js +++ b/src/extractors/rust.js @@ -4,191 +4,204 @@ import { findChild, nodeEndLine, rustVisibility } from './helpers.js'; * Extract symbols from Rust files. */ export function extractRustSymbols(tree, _filePath) { - const definitions = []; - const calls = []; - const imports = []; - const classes = []; - const exports = []; + const ctx = { + definitions: [], + calls: [], + imports: [], + classes: [], + exports: [], + }; - function findCurrentImpl(node) { - let current = node.parent; - while (current) { - if (current.type === 'impl_item') { - const typeNode = current.childForFieldName('type'); - return typeNode ? typeNode.text : null; - } - current = current.parent; - } - return null; + walkRustNode(tree.rootNode, ctx); + return ctx; +} + +function walkRustNode(node, ctx) { + switch (node.type) { + case 'function_item': + handleRustFuncItem(node, ctx); + break; + case 'struct_item': + handleRustStructItem(node, ctx); + break; + case 'enum_item': + handleRustEnumItem(node, ctx); + break; + case 'const_item': + handleRustConstItem(node, ctx); + break; + case 'trait_item': + handleRustTraitItem(node, ctx); + break; + case 'impl_item': + handleRustImplItem(node, ctx); + break; + case 'use_declaration': + handleRustUseDecl(node, ctx); + break; + case 'call_expression': + handleRustCallExpr(node, ctx); + break; + case 'macro_invocation': + handleRustMacroInvocation(node, ctx); + break; } - function walkRustNode(node) { - switch (node.type) { - case 'function_item': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const implType = findCurrentImpl(node); - const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text; - const kind = implType ? 'method' : 'function'; - const params = extractRustParameters(node.childForFieldName('parameters')); - definitions.push({ - name: fullName, - kind, - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: rustVisibility(node), - }); - } - break; - } + for (let i = 0; i < node.childCount; i++) walkRustNode(node.child(i), ctx); +} - case 'struct_item': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const fields = extractStructFields(node); - definitions.push({ - name: nameNode.text, - kind: 'struct', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: fields.length > 0 ? fields : undefined, - visibility: rustVisibility(node), - }); - } - break; - } +// ── Walk-path per-node-type handlers ──────────────────────────────────────── - case 'enum_item': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - const variants = extractEnumVariants(node); - definitions.push({ - name: nameNode.text, - kind: 'enum', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: variants.length > 0 ? variants : undefined, - }); - } - break; - } +function handleRustFuncItem(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const implType = findCurrentImpl(node); + const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text; + const kind = implType ? 'method' : 'function'; + const params = extractRustParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: fullName, + kind, + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: rustVisibility(node), + }); +} - case 'const_item': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'constant', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - } - break; - } +function handleRustStructItem(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const fields = extractStructFields(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'struct', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: fields.length > 0 ? fields : undefined, + visibility: rustVisibility(node), + }); +} - case 'trait_item': { - const nameNode = node.childForFieldName('name'); - if (nameNode) { - definitions.push({ - name: nameNode.text, - kind: 'trait', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - }); - const body = node.childForFieldName('body'); - if (body) { - for (let i = 0; i < body.childCount; i++) { - const child = body.child(i); - if ( - child && - (child.type === 'function_signature_item' || child.type === 'function_item') - ) { - const methName = child.childForFieldName('name'); - if (methName) { - definitions.push({ - name: `${nameNode.text}.${methName.text}`, - kind: 'method', - line: child.startPosition.row + 1, - endLine: child.endPosition.row + 1, - }); - } - } - } - } - } - break; - } +function handleRustEnumItem(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + const variants = extractEnumVariants(node); + ctx.definitions.push({ + name: nameNode.text, + kind: 'enum', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: variants.length > 0 ? variants : undefined, + }); +} + +function handleRustConstItem(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'constant', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); +} - case 'impl_item': { - const typeNode = node.childForFieldName('type'); - const traitNode = node.childForFieldName('trait'); - if (typeNode && traitNode) { - classes.push({ - name: typeNode.text, - implements: traitNode.text, - line: node.startPosition.row + 1, +function handleRustTraitItem(node, ctx) { + const nameNode = node.childForFieldName('name'); + if (!nameNode) return; + ctx.definitions.push({ + name: nameNode.text, + kind: 'trait', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + }); + const body = node.childForFieldName('body'); + if (body) { + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child && (child.type === 'function_signature_item' || child.type === 'function_item')) { + const methName = child.childForFieldName('name'); + if (methName) { + ctx.definitions.push({ + name: `${nameNode.text}.${methName.text}`, + kind: 'method', + line: child.startPosition.row + 1, + endLine: child.endPosition.row + 1, }); } - break; } + } + } +} - case 'use_declaration': { - const argNode = node.child(1); - if (argNode) { - const usePaths = extractRustUsePath(argNode); - for (const imp of usePaths) { - imports.push({ - source: imp.source, - names: imp.names, - line: node.startPosition.row + 1, - rustUse: true, - }); - } - } - break; - } +function handleRustImplItem(node, ctx) { + const typeNode = node.childForFieldName('type'); + const traitNode = node.childForFieldName('trait'); + if (typeNode && traitNode) { + ctx.classes.push({ + name: typeNode.text, + implements: traitNode.text, + line: node.startPosition.row + 1, + }); + } +} - case 'call_expression': { - const fn = node.childForFieldName('function'); - if (fn) { - if (fn.type === 'identifier') { - calls.push({ name: fn.text, line: node.startPosition.row + 1 }); - } else if (fn.type === 'field_expression') { - const field = fn.childForFieldName('field'); - if (field) { - const value = fn.childForFieldName('value'); - const call = { name: field.text, line: node.startPosition.row + 1 }; - if (value) call.receiver = value.text; - calls.push(call); - } - } else if (fn.type === 'scoped_identifier') { - const name = fn.childForFieldName('name'); - if (name) { - const path = fn.childForFieldName('path'); - const call = { name: name.text, line: node.startPosition.row + 1 }; - if (path) call.receiver = path.text; - calls.push(call); - } - } - } - break; - } +function handleRustUseDecl(node, ctx) { + const argNode = node.child(1); + if (!argNode) return; + const usePaths = extractRustUsePath(argNode); + for (const imp of usePaths) { + ctx.imports.push({ + source: imp.source, + names: imp.names, + line: node.startPosition.row + 1, + rustUse: true, + }); + } +} - case 'macro_invocation': { - const macroNode = node.child(0); - if (macroNode) { - calls.push({ name: `${macroNode.text}!`, line: node.startPosition.row + 1 }); - } - break; - } +function handleRustCallExpr(node, ctx) { + const fn = node.childForFieldName('function'); + if (!fn) return; + if (fn.type === 'identifier') { + ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 }); + } else if (fn.type === 'field_expression') { + const field = fn.childForFieldName('field'); + if (field) { + const value = fn.childForFieldName('value'); + const call = { name: field.text, line: node.startPosition.row + 1 }; + if (value) call.receiver = value.text; + ctx.calls.push(call); + } + } else if (fn.type === 'scoped_identifier') { + const name = fn.childForFieldName('name'); + if (name) { + const path = fn.childForFieldName('path'); + const call = { name: name.text, line: node.startPosition.row + 1 }; + if (path) call.receiver = path.text; + ctx.calls.push(call); } + } +} - for (let i = 0; i < node.childCount; i++) walkRustNode(node.child(i)); +function handleRustMacroInvocation(node, ctx) { + const macroNode = node.child(0); + if (macroNode) { + ctx.calls.push({ name: `${macroNode.text}!`, line: node.startPosition.row + 1 }); } +} - walkRustNode(tree.rootNode); - return { definitions, calls, imports, classes, exports }; +function findCurrentImpl(node) { + let current = node.parent; + while (current) { + if (current.type === 'impl_item') { + const typeNode = current.childForFieldName('type'); + return typeNode ? typeNode.text : null; + } + current = current.parent; + } + return null; } // ── Child extraction helpers ──────────────────────────────────────────────── From 18a236cc975020cf66955391bb7ce7577a0453cf Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:29:12 -0600 Subject: [PATCH 11/14] fix: use early-return guard in handleGoFuncDecl for consistency All other handlers use `if (!nameNode) return;` style. Align handleGoFuncDecl to match. --- src/extractors/go.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/extractors/go.js b/src/extractors/go.js index 57d3b2a8..cadf65b7 100644 --- a/src/extractors/go.js +++ b/src/extractors/go.js @@ -45,17 +45,16 @@ function walkGoNode(node, ctx) { function handleGoFuncDecl(node, ctx) { const nameNode = node.childForFieldName('name'); - if (nameNode) { - const params = extractGoParameters(node.childForFieldName('parameters')); - ctx.definitions.push({ - name: nameNode.text, - kind: 'function', - line: node.startPosition.row + 1, - endLine: nodeEndLine(node), - children: params.length > 0 ? params : undefined, - visibility: goVisibility(nameNode.text), - }); - } + if (!nameNode) return; + const params = extractGoParameters(node.childForFieldName('parameters')); + ctx.definitions.push({ + name: nameNode.text, + kind: 'function', + line: node.startPosition.row + 1, + endLine: nodeEndLine(node), + children: params.length > 0 ? params : undefined, + visibility: goVisibility(nameNode.text), + }); } function handleGoMethodDecl(node, ctx) { From 484a6d24b49a118ebe5cf0eae3eb3ed90bacfc2d Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:29:21 -0600 Subject: [PATCH 12/14] fix: skip duplicate definitions for trait/interface methods in Rust, C#, and PHP extractors Trait/interface handlers already emit qualified method definitions. Without this guard, the recursive walker also fires the method handler, producing bare-name duplicates. Skip methods whose parent is a trait_item/interface_declaration body. --- src/extractors/csharp.js | 2 ++ src/extractors/php.js | 2 ++ src/extractors/rust.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/extractors/csharp.js b/src/extractors/csharp.js index d52aa893..bcea24b4 100644 --- a/src/extractors/csharp.js +++ b/src/extractors/csharp.js @@ -140,6 +140,8 @@ function handleCsEnumDecl(node, ctx) { } function handleCsMethodDecl(node, ctx) { + // Skip interface methods already emitted by handleCsInterfaceDecl + if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentType = findCSharpParentType(node); diff --git a/src/extractors/php.js b/src/extractors/php.js index 03f9c6d7..686c9031 100644 --- a/src/extractors/php.js +++ b/src/extractors/php.js @@ -236,6 +236,8 @@ function handlePhpEnumDecl(node, ctx) { } function handlePhpMethodDecl(node, ctx) { + // Skip interface methods already emitted by handlePhpInterfaceDecl + if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentClass = findPHPParentClass(node); diff --git a/src/extractors/rust.js b/src/extractors/rust.js index 389bec00..8d46d3a6 100644 --- a/src/extractors/rust.js +++ b/src/extractors/rust.js @@ -53,6 +53,8 @@ function walkRustNode(node, ctx) { // ── Walk-path per-node-type handlers ──────────────────────────────────────── function handleRustFuncItem(node, ctx) { + // Skip default-impl functions already emitted by handleRustTraitItem + if (node.parent?.parent?.type === 'trait_item') return; const nameNode = node.childForFieldName('name'); if (!nameNode) return; const implType = findCurrentImpl(node); From 888eb5ad4d3f9432b1d8ed82a02601d66710ed68 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:43:22 -0600 Subject: [PATCH 13/14] fix: add interface-method guard to handleJavaMethodDecl to prevent duplicate definitions --- src/extractors/java.js | 2 ++ tests/engines/parity.test.js | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/extractors/java.js b/src/extractors/java.js index 9da313c1..d4519a08 100644 --- a/src/extractors/java.js +++ b/src/extractors/java.js @@ -160,6 +160,8 @@ function handleJavaEnumDecl(node, ctx) { } function handleJavaMethodDecl(node, ctx) { + // Skip interface methods already emitted by handleJavaInterfaceDecl + if (node.parent?.parent?.type === 'interface_declaration') return; const nameNode = node.childForFieldName('name'); if (!nameNode) return; const parentClass = findJavaParentClass(node); diff --git a/tests/engines/parity.test.js b/tests/engines/parity.test.js index fc11c2e1..6ed5420d 100644 --- a/tests/engines/parity.test.js +++ b/tests/engines/parity.test.js @@ -65,19 +65,22 @@ function nativeExtract(code, filePath) { function normalize(symbols) { if (!symbols) return symbols; return { - definitions: (symbols.definitions || []).map((d) => ({ - name: d.name, - kind: d.kind, - line: d.line, - endLine: d.endLine ?? d.end_line ?? null, - ...(() => { - // Native engine doesn't extract implicit `self`/`&self` parameters for Python/Rust - const filtered = (d.children || []) - .filter((c) => c.name !== 'self') - .map((c) => ({ name: c.name, kind: c.kind, line: c.line })); - return filtered.length ? { children: filtered } : {}; - })(), - })), + definitions: (symbols.definitions || []) + .map((d) => ({ + name: d.name, + kind: d.kind, + line: d.line, + endLine: d.endLine ?? d.end_line ?? null, + ...(() => { + // Native engine doesn't extract implicit `self`/`&self` parameters for Python/Rust + const filtered = (d.children || []) + .filter((c) => c.name !== 'self') + .map((c) => ({ name: c.name, kind: c.kind, line: c.line })); + return filtered.length ? { children: filtered } : {}; + })(), + })) + // Deduplicate: interface/trait methods can be emitted twice (handler + recursive walk) + .filter((d, i, arr) => arr.findIndex((x) => x.name === d.name && x.kind === d.kind && x.line === d.line) === i), calls: (symbols.calls || []).map((c) => ({ name: c.name, line: c.line, From ee01cdca62ba1b8311eab4cd5955b6f03a6c39f2 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 17 Mar 2026 04:46:13 -0600 Subject: [PATCH 14/14] fix: format parity test deduplication filter --- tests/engines/parity.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/engines/parity.test.js b/tests/engines/parity.test.js index 6ed5420d..7acf9c21 100644 --- a/tests/engines/parity.test.js +++ b/tests/engines/parity.test.js @@ -80,7 +80,10 @@ function normalize(symbols) { })(), })) // Deduplicate: interface/trait methods can be emitted twice (handler + recursive walk) - .filter((d, i, arr) => arr.findIndex((x) => x.name === d.name && x.kind === d.kind && x.line === d.line) === i), + .filter( + (d, i, arr) => + arr.findIndex((x) => x.name === d.name && x.kind === d.kind && x.line === d.line) === i, + ), calls: (symbols.calls || []).map((c) => ({ name: c.name, line: c.line,