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/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..ecafa49e 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -242,11 +242,23 @@ 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) { + if (!hasTable(db, 'build_meta')) return null; try { const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key); return row ? row.value : null; - } catch { + } catch (e) { + debug(`getBuildMeta failed for key "${key}": ${e.message}`); return null; } } @@ -280,74 +292,39 @@ export function initSchema(db) { } } - try { - db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE nodes ADD COLUMN role TEXT'); - } catch { - /* already exists */ - } - try { + // Legacy column compat — add columns that may be missing from pre-migration DBs + if (hasTable(db, 'nodes')) { + if (!hasColumn(db, 'nodes', 'end_line')) { + db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER'); + } + if (!hasColumn(db, 'nodes', 'role')) { + db.exec('ALTER TABLE nodes ADD COLUMN role TEXT'); + } db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)'); - } catch { - /* already exists */ - } - try { + if (!hasColumn(db, 'nodes', 'parent_id')) { + db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)'); + } 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('ALTER TABLE nodes ADD COLUMN qualified_name TEXT'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT'); - } catch { - /* already exists */ - } - try { - db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT'); - } catch { - /* already exists */ - } - try { + if (!hasColumn(db, 'nodes', 'qualified_name')) { + db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT'); + } + if (!hasColumn(db, 'nodes', 'scope')) { + db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT'); + } + if (!hasColumn(db, 'nodes', 'visibility')) { + db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT'); + } 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 */ + } + if (hasTable(db, 'edges')) { + if (!hasColumn(db, 'edges', 'confidence')) { + db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0'); + } + if (!hasColumn(db, 'edges', 'dynamic')) { + db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0'); + } } } 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)); 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/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 }; } diff --git a/src/features/cfg.js b/src/features/cfg.js index e8728cab..ae1b8564 100644 --- a/src/features/cfg.js +++ b/src/features/cfg.js @@ -23,9 +23,9 @@ import { hasCfgTables, openReadonlyOrFail, } from '../db/index.js'; -import { info } from '../infrastructure/logger.js'; -import { isTestFile } from '../infrastructure/test-filter.js'; +import { debug, info } from '../infrastructure/logger.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 }; @@ -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; } } @@ -273,27 +275,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 +299,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/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 9d0c8bcc..695afa95 100644 --- a/src/features/dataflow.js +++ b/src/features/dataflow.js @@ -21,9 +21,10 @@ 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'; // Re-export for backward compatibility export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES }; @@ -140,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; } @@ -149,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; } } @@ -234,31 +237,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 +261,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 +410,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 +548,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..d888f548 --- /dev/null +++ b/src/features/shared/find-nodes.js @@ -0,0 +1,33 @@ +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; + if (kinds.length === 0) throw new Error('findNodes: no kinds specified'); + 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; +} 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))}`; -}