@optave/codegraph 3.1.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -72
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +260 -246
- package/src/ast-analysis/shared.js +2 -14
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +4 -7
- package/src/cli/commands/audit.js +11 -11
- package/src/cli/commands/batch.js +6 -5
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +5 -8
- package/src/cli/commands/check.js +28 -36
- package/src/cli/commands/children.js +9 -7
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +5 -3
- package/src/cli/commands/context.js +9 -8
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +5 -8
- package/src/cli/commands/deps.js +9 -8
- package/src/cli/commands/diff-impact.js +2 -6
- package/src/cli/commands/embed.js +1 -1
- package/src/cli/commands/export.js +34 -31
- package/src/cli/commands/exports.js +2 -6
- package/src/cli/commands/flow.js +5 -8
- package/src/cli/commands/fn-impact.js +9 -8
- package/src/cli/commands/impact.js +2 -6
- package/src/cli/commands/info.js +2 -2
- package/src/cli/commands/map.js +1 -1
- package/src/cli/commands/mcp.js +1 -1
- package/src/cli/commands/models.js +1 -1
- package/src/cli/commands/owners.js +5 -3
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +9 -8
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +5 -8
- package/src/cli/commands/search.js +9 -3
- package/src/cli/commands/sequence.js +5 -8
- package/src/cli/commands/snapshot.js +6 -1
- package/src/cli/commands/stats.js +1 -1
- package/src/cli/commands/structure.js +5 -4
- package/src/cli/commands/triage.js +41 -30
- package/src/cli/commands/watch.js +1 -1
- package/src/cli/commands/where.js +2 -6
- package/src/cli/index.js +11 -5
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +22 -2
- package/src/cli.js +1 -1
- package/src/db/connection.js +140 -11
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +42 -65
- package/src/db/query-builder.js +72 -9
- package/src/db/repository/base.js +1 -1
- package/src/db/repository/graph-read.js +3 -3
- package/src/db/repository/in-memory-repository.js +30 -28
- package/src/db/repository/nodes.js +10 -17
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +392 -0
- package/src/domain/analysis/dependencies.js +395 -0
- package/src/{analysis → domain/analysis}/exports.js +11 -6
- package/src/domain/analysis/impact.js +581 -0
- package/src/domain/analysis/module-map.js +348 -0
- package/src/{analysis → domain/analysis}/roles.js +12 -9
- package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
- package/src/{builder → domain/graph/builder}/helpers.js +4 -4
- package/src/{builder → domain/graph/builder}/incremental.js +119 -93
- package/src/domain/graph/builder/pipeline.js +156 -0
- package/src/domain/graph/builder/stages/build-edges.js +376 -0
- package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
- package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
- package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/{cycles.js → domain/graph/cycles.js} +4 -4
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +2 -2
- package/src/{watcher.js → domain/graph/watcher.js} +7 -7
- package/src/{parser.js → domain/parser.js} +24 -15
- package/src/{queries.js → domain/queries.js} +17 -16
- package/src/{embeddings → domain/search}/generator.js +3 -3
- package/src/{embeddings → domain/search}/models.js +2 -2
- package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
- package/src/{embeddings → domain/search}/search/filters.js +9 -5
- package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +13 -6
- package/src/{embeddings → domain/search}/search/prepare.js +15 -7
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +275 -305
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/{ast.js → features/ast.js} +13 -11
- package/src/{audit.js → features/audit.js} +20 -46
- package/src/{batch.js → features/batch.js} +5 -5
- package/src/{boundaries.js → features/boundaries.js} +100 -85
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +141 -150
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +72 -57
- package/src/{complexity.js → features/complexity.js} +154 -143
- package/src/{dataflow.js → features/dataflow.js} +155 -158
- package/src/{export.js → features/export.js} +6 -6
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +8 -8
- package/src/{manifesto.js → features/manifesto.js} +15 -12
- package/src/{owners.js → features/owners.js} +6 -5
- package/src/features/sequence.js +300 -0
- package/src/features/shared/find-nodes.js +31 -0
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +139 -108
- package/src/features/triage.js +141 -0
- package/src/graph/builders/dependency.js +33 -14
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.cjs +16 -0
- package/src/index.js +40 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +68 -59
- package/src/mcp/tool-registry.js +15 -2
- package/src/mcp/tools/ast-query.js +1 -1
- package/src/mcp/tools/audit.js +1 -1
- package/src/mcp/tools/batch-query.js +1 -1
- package/src/mcp/tools/branch-compare.js +3 -1
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/cfg.js +1 -1
- package/src/mcp/tools/check.js +3 -3
- package/src/mcp/tools/co-changes.js +1 -1
- package/src/mcp/tools/code-owners.js +1 -1
- package/src/mcp/tools/communities.js +1 -1
- package/src/mcp/tools/complexity.js +1 -1
- package/src/mcp/tools/dataflow.js +2 -2
- package/src/mcp/tools/execution-flow.js +2 -2
- package/src/mcp/tools/export-graph.js +2 -2
- package/src/mcp/tools/find-cycles.js +2 -2
- package/src/mcp/tools/index.js +2 -0
- package/src/mcp/tools/list-repos.js +1 -1
- package/src/mcp/tools/sequence.js +1 -1
- package/src/mcp/tools/structure.js +1 -1
- package/src/mcp/tools/triage.js +2 -2
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/presentation/brief.js +51 -0
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +2 -2
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +1 -1
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +53 -0
- package/src/presentation/queries-cli/impact.js +214 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +329 -0
- package/src/presentation/queries-cli/overview.js +196 -0
- package/src/presentation/queries-cli/path.js +65 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- package/src/presentation/table.js +0 -8
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/file-utils.js +2 -2
- package/src/shared/generators.js +9 -5
- package/src/shared/hierarchy.js +1 -1
- package/src/{kinds.js → shared/kinds.js} +1 -1
- package/src/analysis/context.js +0 -408
- package/src/analysis/dependencies.js +0 -341
- package/src/analysis/impact.js +0 -463
- package/src/analysis/module-map.js +0 -322
- package/src/builder/pipeline.js +0 -130
- package/src/builder/stages/build-edges.js +0 -297
- package/src/builder/stages/insert-nodes.js +0 -195
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- package/src/sequence.js +0 -289
- package/src/triage.js +0 -126
- /package/src/{builder → domain/graph/builder}/context.js +0 -0
- /package/src/{builder.js → domain/graph/builder.js} +0 -0
- /package/src/{embeddings → domain/search}/index.js +0 -0
- /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
- /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{errors.js → shared/errors.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { performance } from 'node:perf_hooks';
|
|
20
|
-
import { bulkNodeIdsByFile } from '../db.js';
|
|
21
|
-
import { debug } from '../logger.js';
|
|
20
|
+
import { bulkNodeIdsByFile } from '../db/index.js';
|
|
21
|
+
import { debug } from '../infrastructure/logger.js';
|
|
22
22
|
import { computeLOCMetrics, computeMaintainabilityIndex } from './metrics.js';
|
|
23
23
|
import {
|
|
24
24
|
AST_TYPE_MAPS,
|
|
@@ -38,6 +38,7 @@ import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
|
|
|
38
38
|
// ─── Extension sets for quick language-support checks ────────────────────
|
|
39
39
|
|
|
40
40
|
const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
|
|
41
|
+
const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
|
|
41
42
|
const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
|
|
42
43
|
const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
|
|
43
44
|
|
|
@@ -45,282 +46,234 @@ const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
|
|
|
45
46
|
|
|
46
47
|
let _parserModule = null;
|
|
47
48
|
async function getParserModule() {
|
|
48
|
-
if (!_parserModule) _parserModule = await import('../parser.js');
|
|
49
|
+
if (!_parserModule) _parserModule = await import('../domain/parser.js');
|
|
49
50
|
return _parserModule;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
// ───
|
|
53
|
+
// ─── WASM pre-parse ─────────────────────────────────────────────────────
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
* Run all enabled AST analyses in a coordinated pass.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} db - open better-sqlite3 database (read-write)
|
|
58
|
-
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
|
|
59
|
-
* @param {string} rootDir - absolute project root path
|
|
60
|
-
* @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
|
|
61
|
-
* @param {object} [engineOpts] - engine options
|
|
62
|
-
* @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
|
|
63
|
-
*/
|
|
64
|
-
export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
65
|
-
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
66
|
-
|
|
67
|
-
const doAst = opts.ast !== false;
|
|
55
|
+
async function ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir) {
|
|
68
56
|
const doComplexity = opts.complexity !== false;
|
|
69
57
|
const doCfg = opts.cfg !== false;
|
|
70
58
|
const doDataflow = opts.dataflow !== false;
|
|
71
59
|
|
|
72
|
-
if (!
|
|
73
|
-
|
|
74
|
-
const extToLang = buildExtToLangMap();
|
|
75
|
-
|
|
76
|
-
// ── WASM pre-parse for files that need it ───────────────────────────
|
|
77
|
-
// CFG now runs as a visitor in the unified walk, so only dataflow
|
|
78
|
-
// triggers WASM pre-parse when no tree exists.
|
|
79
|
-
if (doDataflow) {
|
|
80
|
-
let needsWasmTrees = false;
|
|
81
|
-
for (const [relPath, symbols] of fileSymbols) {
|
|
82
|
-
if (symbols._tree) continue;
|
|
83
|
-
const ext = path.extname(relPath).toLowerCase();
|
|
84
|
-
|
|
85
|
-
if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
|
|
86
|
-
needsWasmTrees = true;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (needsWasmTrees) {
|
|
92
|
-
try {
|
|
93
|
-
const { ensureWasmTrees } = await getParserModule();
|
|
94
|
-
await ensureWasmTrees(fileSymbols, rootDir);
|
|
95
|
-
} catch (err) {
|
|
96
|
-
debug(`ensureWasmTrees failed: ${err.message}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── Phase 7 Optimization: Unified pre-walk ─────────────────────────
|
|
102
|
-
// For files with WASM trees, run all applicable visitors in a SINGLE
|
|
103
|
-
// walkWithVisitors call. Store results in the format that buildXxx
|
|
104
|
-
// functions already expect as pre-computed data (same fields as native
|
|
105
|
-
// engine output). This eliminates ~3 redundant tree traversals per file.
|
|
106
|
-
const t0walk = performance.now();
|
|
60
|
+
if (!doComplexity && !doCfg && !doDataflow) return;
|
|
107
61
|
|
|
62
|
+
let needsWasmTrees = false;
|
|
108
63
|
for (const [relPath, symbols] of fileSymbols) {
|
|
109
|
-
if (
|
|
110
|
-
|
|
64
|
+
if (symbols._tree) continue;
|
|
111
65
|
const ext = path.extname(relPath).toLowerCase();
|
|
112
|
-
const langId = symbols._langId || extToLang.get(ext);
|
|
113
|
-
if (!langId) continue;
|
|
114
|
-
|
|
115
66
|
const defs = symbols.definitions || [];
|
|
116
|
-
const visitors = [];
|
|
117
|
-
const walkerOpts = {
|
|
118
|
-
functionNodeTypes: new Set(),
|
|
119
|
-
nestingNodeTypes: new Set(),
|
|
120
|
-
getFunctionName: (_node) => null,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// ─ AST-store visitor ─
|
|
124
|
-
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
125
|
-
let astVisitor = null;
|
|
126
|
-
if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
|
|
127
|
-
const nodeIdMap = new Map();
|
|
128
|
-
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
129
|
-
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
130
|
-
}
|
|
131
|
-
astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
|
|
132
|
-
visitors.push(astVisitor);
|
|
133
|
-
}
|
|
134
67
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
if (needsWasmComplexity) {
|
|
145
|
-
complexityVisitor = createComplexityVisitor(cRules, hRules, {
|
|
146
|
-
fileLevelWalk: true,
|
|
147
|
-
langId,
|
|
148
|
-
});
|
|
149
|
-
visitors.push(complexityVisitor);
|
|
150
|
-
|
|
151
|
-
// Merge nesting nodes for complexity tracking
|
|
152
|
-
// NOTE: do NOT add functionNodes here — funcDepth in the complexity
|
|
153
|
-
// visitor already tracks function-level nesting. Adding them to
|
|
154
|
-
// nestingNodeTypes would inflate context.nestingLevel by +1 inside
|
|
155
|
-
// every function body, double-counting in cognitive += 1 + nestingLevel.
|
|
156
|
-
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
|
|
157
|
-
|
|
158
|
-
// Provide getFunctionName for complexity visitor
|
|
159
|
-
const dfRules = DATAFLOW_RULES.get(langId);
|
|
160
|
-
walkerOpts.getFunctionName = (node) => {
|
|
161
|
-
// Try complexity rules' function name field first
|
|
162
|
-
const nameNode = node.childForFieldName('name');
|
|
163
|
-
if (nameNode) return nameNode.text;
|
|
164
|
-
// Fall back to dataflow rules' richer name extraction
|
|
165
|
-
if (dfRules) return getFuncName(node, dfRules);
|
|
166
|
-
return null;
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ─ CFG visitor ─
|
|
172
|
-
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
173
|
-
let cfgVisitor = null;
|
|
174
|
-
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
|
|
175
|
-
// Only use visitor if some functions lack pre-computed CFG
|
|
176
|
-
const needsWasmCfg = defs.some(
|
|
68
|
+
const needsComplexity =
|
|
69
|
+
doComplexity &&
|
|
70
|
+
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
71
|
+
defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
|
|
72
|
+
const needsCfg =
|
|
73
|
+
doCfg &&
|
|
74
|
+
CFG_EXTENSIONS.has(ext) &&
|
|
75
|
+
defs.some(
|
|
177
76
|
(d) =>
|
|
178
77
|
(d.kind === 'function' || d.kind === 'method') &&
|
|
179
78
|
d.line &&
|
|
180
79
|
d.cfg !== null &&
|
|
181
80
|
!Array.isArray(d.cfg?.blocks),
|
|
182
81
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
82
|
+
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
|
|
83
|
+
|
|
84
|
+
if (needsComplexity || needsCfg || needsDataflow) {
|
|
85
|
+
needsWasmTrees = true;
|
|
86
|
+
break;
|
|
187
87
|
}
|
|
88
|
+
}
|
|
188
89
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
90
|
+
if (needsWasmTrees) {
|
|
91
|
+
try {
|
|
92
|
+
const { ensureWasmTrees } = await getParserModule();
|
|
93
|
+
await ensureWasmTrees(fileSymbols, rootDir);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
debug(`ensureWasmTrees failed: ${err.message}`);
|
|
195
96
|
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
196
99
|
|
|
197
|
-
|
|
198
|
-
if (visitors.length === 0) continue;
|
|
100
|
+
// ─── Per-file visitor setup ─────────────────────────────────────────────
|
|
199
101
|
|
|
200
|
-
|
|
102
|
+
function setupVisitors(db, relPath, symbols, langId, opts) {
|
|
103
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
104
|
+
const defs = symbols.definitions || [];
|
|
105
|
+
const doAst = opts.ast !== false;
|
|
106
|
+
const doComplexity = opts.complexity !== false;
|
|
107
|
+
const doCfg = opts.cfg !== false;
|
|
108
|
+
const doDataflow = opts.dataflow !== false;
|
|
201
109
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
110
|
+
const visitors = [];
|
|
111
|
+
const walkerOpts = {
|
|
112
|
+
functionNodeTypes: new Set(),
|
|
113
|
+
nestingNodeTypes: new Set(),
|
|
114
|
+
getFunctionName: (_node) => null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// AST-store visitor
|
|
118
|
+
let astVisitor = null;
|
|
119
|
+
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
120
|
+
if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
|
|
121
|
+
const nodeIdMap = new Map();
|
|
122
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
123
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
209
124
|
}
|
|
125
|
+
astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
|
|
126
|
+
visitors.push(astVisitor);
|
|
127
|
+
}
|
|
210
128
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const n = r.funcNode.childForFieldName('name');
|
|
233
|
-
return n && n.text === def.name;
|
|
234
|
-
}) ?? candidates[0]);
|
|
235
|
-
if (funcResult) {
|
|
236
|
-
const { metrics } = funcResult;
|
|
237
|
-
const loc = computeLOCMetrics(funcResult.funcNode, langId);
|
|
238
|
-
const volume = metrics.halstead ? metrics.halstead.volume : 0;
|
|
239
|
-
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
240
|
-
const mi = computeMaintainabilityIndex(
|
|
241
|
-
volume,
|
|
242
|
-
metrics.cyclomatic,
|
|
243
|
-
loc.sloc,
|
|
244
|
-
commentRatio,
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
def.complexity = {
|
|
248
|
-
cognitive: metrics.cognitive,
|
|
249
|
-
cyclomatic: metrics.cyclomatic,
|
|
250
|
-
maxNesting: metrics.maxNesting,
|
|
251
|
-
halstead: metrics.halstead,
|
|
252
|
-
loc,
|
|
253
|
-
maintainabilityIndex: mi,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
129
|
+
// Complexity visitor (file-level mode)
|
|
130
|
+
let complexityVisitor = null;
|
|
131
|
+
const cRules = COMPLEXITY_RULES.get(langId);
|
|
132
|
+
const hRules = HALSTEAD_RULES.get(langId);
|
|
133
|
+
if (doComplexity && cRules) {
|
|
134
|
+
const needsWasmComplexity = defs.some(
|
|
135
|
+
(d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
|
|
136
|
+
);
|
|
137
|
+
if (needsWasmComplexity) {
|
|
138
|
+
complexityVisitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
|
|
139
|
+
visitors.push(complexityVisitor);
|
|
140
|
+
|
|
141
|
+
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
|
|
142
|
+
|
|
143
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
144
|
+
walkerOpts.getFunctionName = (node) => {
|
|
145
|
+
const nameNode = node.childForFieldName('name');
|
|
146
|
+
if (nameNode) return nameNode.text;
|
|
147
|
+
if (dfRules) return getFuncName(node, dfRules);
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
258
150
|
}
|
|
151
|
+
}
|
|
259
152
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def.line &&
|
|
275
|
-
!def.cfg?.blocks?.length
|
|
276
|
-
) {
|
|
277
|
-
const candidates = cfgByLine.get(def.line);
|
|
278
|
-
const cfgResult = !candidates
|
|
279
|
-
? undefined
|
|
280
|
-
: candidates.length === 1
|
|
281
|
-
? candidates[0]
|
|
282
|
-
: (candidates.find((r) => {
|
|
283
|
-
const n = r.funcNode.childForFieldName('name');
|
|
284
|
-
return n && n.text === def.name;
|
|
285
|
-
}) ?? candidates[0]);
|
|
286
|
-
if (cfgResult) {
|
|
287
|
-
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
288
|
-
|
|
289
|
-
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
290
|
-
// and recompute maintainability index to stay consistent
|
|
291
|
-
if (def.complexity && cfgResult.cyclomatic != null) {
|
|
292
|
-
def.complexity.cyclomatic = cfgResult.cyclomatic;
|
|
293
|
-
const { loc, halstead } = def.complexity;
|
|
294
|
-
const volume = halstead ? halstead.volume : 0;
|
|
295
|
-
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
296
|
-
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
297
|
-
volume,
|
|
298
|
-
cfgResult.cyclomatic,
|
|
299
|
-
loc?.sloc ?? 0,
|
|
300
|
-
commentRatio,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
153
|
+
// CFG visitor
|
|
154
|
+
let cfgVisitor = null;
|
|
155
|
+
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
156
|
+
if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
|
|
157
|
+
const needsWasmCfg = defs.some(
|
|
158
|
+
(d) =>
|
|
159
|
+
(d.kind === 'function' || d.kind === 'method') &&
|
|
160
|
+
d.line &&
|
|
161
|
+
d.cfg !== null &&
|
|
162
|
+
!Array.isArray(d.cfg?.blocks),
|
|
163
|
+
);
|
|
164
|
+
if (needsWasmCfg) {
|
|
165
|
+
cfgVisitor = createCfgVisitor(cfgRulesForLang);
|
|
166
|
+
visitors.push(cfgVisitor);
|
|
306
167
|
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Dataflow visitor
|
|
171
|
+
let dataflowVisitor = null;
|
|
172
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
173
|
+
if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
|
|
174
|
+
dataflowVisitor = createDataflowVisitor(dfRules);
|
|
175
|
+
visitors.push(dataflowVisitor);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
|
|
179
|
+
}
|
|
307
180
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
181
|
+
// ─── Result storage helpers ─────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function storeComplexityResults(results, defs, langId) {
|
|
184
|
+
const complexityResults = results.complexity || [];
|
|
185
|
+
const resultByLine = new Map();
|
|
186
|
+
for (const r of complexityResults) {
|
|
187
|
+
if (r.funcNode) {
|
|
188
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
189
|
+
if (!resultByLine.has(line)) resultByLine.set(line, []);
|
|
190
|
+
resultByLine.get(line).push(r);
|
|
311
191
|
}
|
|
312
192
|
}
|
|
193
|
+
for (const def of defs) {
|
|
194
|
+
if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
|
|
195
|
+
const candidates = resultByLine.get(def.line);
|
|
196
|
+
const funcResult = !candidates
|
|
197
|
+
? undefined
|
|
198
|
+
: candidates.length === 1
|
|
199
|
+
? candidates[0]
|
|
200
|
+
: (candidates.find((r) => {
|
|
201
|
+
const n = r.funcNode.childForFieldName('name');
|
|
202
|
+
return n && n.text === def.name;
|
|
203
|
+
}) ?? candidates[0]);
|
|
204
|
+
if (funcResult) {
|
|
205
|
+
const { metrics } = funcResult;
|
|
206
|
+
const loc = computeLOCMetrics(funcResult.funcNode, langId);
|
|
207
|
+
const volume = metrics.halstead ? metrics.halstead.volume : 0;
|
|
208
|
+
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
209
|
+
const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
|
|
210
|
+
|
|
211
|
+
def.complexity = {
|
|
212
|
+
cognitive: metrics.cognitive,
|
|
213
|
+
cyclomatic: metrics.cyclomatic,
|
|
214
|
+
maxNesting: metrics.maxNesting,
|
|
215
|
+
halstead: metrics.halstead,
|
|
216
|
+
loc,
|
|
217
|
+
maintainabilityIndex: mi,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
313
223
|
|
|
314
|
-
|
|
224
|
+
function storeCfgResults(results, defs) {
|
|
225
|
+
const cfgResults = results.cfg || [];
|
|
226
|
+
const cfgByLine = new Map();
|
|
227
|
+
for (const r of cfgResults) {
|
|
228
|
+
if (r.funcNode) {
|
|
229
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
230
|
+
if (!cfgByLine.has(line)) cfgByLine.set(line, []);
|
|
231
|
+
cfgByLine.get(line).push(r);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const def of defs) {
|
|
235
|
+
if (
|
|
236
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
237
|
+
def.line &&
|
|
238
|
+
!def.cfg?.blocks?.length
|
|
239
|
+
) {
|
|
240
|
+
const candidates = cfgByLine.get(def.line);
|
|
241
|
+
const cfgResult = !candidates
|
|
242
|
+
? undefined
|
|
243
|
+
: candidates.length === 1
|
|
244
|
+
? candidates[0]
|
|
245
|
+
: (candidates.find((r) => {
|
|
246
|
+
const n = r.funcNode.childForFieldName('name');
|
|
247
|
+
return n && n.text === def.name;
|
|
248
|
+
}) ?? candidates[0]);
|
|
249
|
+
if (cfgResult) {
|
|
250
|
+
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
251
|
+
|
|
252
|
+
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
253
|
+
if (def.complexity && cfgResult.cyclomatic != null) {
|
|
254
|
+
def.complexity.cyclomatic = cfgResult.cyclomatic;
|
|
255
|
+
const { loc, halstead } = def.complexity;
|
|
256
|
+
const volume = halstead ? halstead.volume : 0;
|
|
257
|
+
const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
258
|
+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
259
|
+
volume,
|
|
260
|
+
cfgResult.cyclomatic,
|
|
261
|
+
loc?.sloc ?? 0,
|
|
262
|
+
commentRatio,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
315
269
|
|
|
316
|
-
|
|
317
|
-
// Each function finds pre-computed data from the unified walk above
|
|
318
|
-
// (or from the native engine) and only does DB writes + native fallback.
|
|
270
|
+
// ─── Build delegation ───────────────────────────────────────────────────
|
|
319
271
|
|
|
320
|
-
|
|
272
|
+
async function delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing) {
|
|
273
|
+
if (opts.ast !== false) {
|
|
321
274
|
const t0 = performance.now();
|
|
322
275
|
try {
|
|
323
|
-
const { buildAstNodes } = await import('../ast.js');
|
|
276
|
+
const { buildAstNodes } = await import('../features/ast.js');
|
|
324
277
|
await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
|
|
325
278
|
} catch (err) {
|
|
326
279
|
debug(`buildAstNodes failed: ${err.message}`);
|
|
@@ -328,10 +281,10 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
328
281
|
timing.astMs = performance.now() - t0;
|
|
329
282
|
}
|
|
330
283
|
|
|
331
|
-
if (
|
|
284
|
+
if (opts.complexity !== false) {
|
|
332
285
|
const t0 = performance.now();
|
|
333
286
|
try {
|
|
334
|
-
const { buildComplexityMetrics } = await import('../complexity.js');
|
|
287
|
+
const { buildComplexityMetrics } = await import('../features/complexity.js');
|
|
335
288
|
await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
|
|
336
289
|
} catch (err) {
|
|
337
290
|
debug(`buildComplexityMetrics failed: ${err.message}`);
|
|
@@ -339,10 +292,10 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
339
292
|
timing.complexityMs = performance.now() - t0;
|
|
340
293
|
}
|
|
341
294
|
|
|
342
|
-
if (
|
|
295
|
+
if (opts.cfg !== false) {
|
|
343
296
|
const t0 = performance.now();
|
|
344
297
|
try {
|
|
345
|
-
const { buildCFGData } = await import('../cfg.js');
|
|
298
|
+
const { buildCFGData } = await import('../features/cfg.js');
|
|
346
299
|
await buildCFGData(db, fileSymbols, rootDir, engineOpts);
|
|
347
300
|
} catch (err) {
|
|
348
301
|
debug(`buildCFGData failed: ${err.message}`);
|
|
@@ -350,16 +303,77 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
|
350
303
|
timing.cfgMs = performance.now() - t0;
|
|
351
304
|
}
|
|
352
305
|
|
|
353
|
-
if (
|
|
306
|
+
if (opts.dataflow !== false) {
|
|
354
307
|
const t0 = performance.now();
|
|
355
308
|
try {
|
|
356
|
-
const { buildDataflowEdges } = await import('../dataflow.js');
|
|
309
|
+
const { buildDataflowEdges } = await import('../features/dataflow.js');
|
|
357
310
|
await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
|
|
358
311
|
} catch (err) {
|
|
359
312
|
debug(`buildDataflowEdges failed: ${err.message}`);
|
|
360
313
|
}
|
|
361
314
|
timing.dataflowMs = performance.now() - t0;
|
|
362
315
|
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Run all enabled AST analyses in a coordinated pass.
|
|
322
|
+
*
|
|
323
|
+
* @param {object} db - open better-sqlite3 database (read-write)
|
|
324
|
+
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
|
|
325
|
+
* @param {string} rootDir - absolute project root path
|
|
326
|
+
* @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
|
|
327
|
+
* @param {object} [engineOpts] - engine options
|
|
328
|
+
* @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
|
|
329
|
+
*/
|
|
330
|
+
export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
|
|
331
|
+
const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
|
|
332
|
+
|
|
333
|
+
const doAst = opts.ast !== false;
|
|
334
|
+
const doComplexity = opts.complexity !== false;
|
|
335
|
+
const doCfg = opts.cfg !== false;
|
|
336
|
+
const doDataflow = opts.dataflow !== false;
|
|
337
|
+
|
|
338
|
+
if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
|
|
339
|
+
|
|
340
|
+
const extToLang = buildExtToLangMap();
|
|
341
|
+
|
|
342
|
+
// WASM pre-parse for files that need it
|
|
343
|
+
await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
|
|
344
|
+
|
|
345
|
+
// Unified pre-walk: run all applicable visitors in a single DFS per file
|
|
346
|
+
const t0walk = performance.now();
|
|
347
|
+
|
|
348
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
349
|
+
if (!symbols._tree) continue;
|
|
350
|
+
|
|
351
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
352
|
+
const langId = symbols._langId || extToLang.get(ext);
|
|
353
|
+
if (!langId) continue;
|
|
354
|
+
|
|
355
|
+
const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
|
|
356
|
+
setupVisitors(db, relPath, symbols, langId, opts);
|
|
357
|
+
|
|
358
|
+
if (visitors.length === 0) continue;
|
|
359
|
+
|
|
360
|
+
const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
|
|
361
|
+
const defs = symbols.definitions || [];
|
|
362
|
+
|
|
363
|
+
if (astVisitor) {
|
|
364
|
+
const astRows = results['ast-store'] || [];
|
|
365
|
+
if (astRows.length > 0) symbols.astNodes = astRows;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (complexityVisitor) storeComplexityResults(results, defs, langId);
|
|
369
|
+
if (cfgVisitor) storeCfgResults(results, defs);
|
|
370
|
+
if (dataflowVisitor) symbols.dataflow = results.dataflow;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
374
|
+
|
|
375
|
+
// Delegate to buildXxx functions for DB writes + native fallback
|
|
376
|
+
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
|
|
363
377
|
|
|
364
378
|
return timing;
|
|
365
379
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Shared utilities for AST analysis modules (complexity, CFG, dataflow, AST nodes).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { LANGUAGE_REGISTRY } from '../domain/parser.js';
|
|
6
|
+
import { ConfigError } from '../shared/errors.js';
|
|
7
7
|
|
|
8
8
|
// ─── Generic Rule Factory ─────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -176,18 +176,6 @@ export function findFunctionNode(rootNode, startLine, _endLine, rules) {
|
|
|
176
176
|
return best;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/**
|
|
180
|
-
* Truncate a string to a maximum length, appending an ellipsis if truncated.
|
|
181
|
-
*
|
|
182
|
-
* @param {string} str - Input string
|
|
183
|
-
* @param {number} [max=200] - Maximum length
|
|
184
|
-
* @returns {string}
|
|
185
|
-
*/
|
|
186
|
-
export function truncate(str, max = 200) {
|
|
187
|
-
if (!str) return '';
|
|
188
|
-
return str.length > max ? `${str.slice(0, max)}…` : str;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
179
|
// ─── Extension / Language Mapping ─────────────────────────────────────────
|
|
192
180
|
|
|
193
181
|
/**
|