@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { debug } from '../infrastructure/logger.js';
|
|
2
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
3
3
|
|
|
4
4
|
// ─── Glob-to-Regex ───────────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -94,104 +94,119 @@ export function resolveModules(boundaryConfig) {
|
|
|
94
94
|
// ─── Validation ──────────────────────────────────────────────────────
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Validate a boundary
|
|
98
|
-
* @param {object}
|
|
99
|
-
* @
|
|
97
|
+
* Validate the `modules` section of a boundary config.
|
|
98
|
+
* @param {object} modules
|
|
99
|
+
* @param {string[]} errors - Mutated: push any validation errors
|
|
100
100
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
function validateModules(modules, errors) {
|
|
102
|
+
if (!modules || typeof modules !== 'object' || Object.keys(modules).length === 0) {
|
|
103
|
+
errors.push('boundaries.modules must be a non-empty object');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const [name, value] of Object.entries(modules)) {
|
|
107
|
+
if (typeof value === 'string') continue;
|
|
108
|
+
if (value && typeof value === 'object' && typeof value.match === 'string') continue;
|
|
109
|
+
errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Validate the `preset` field of a boundary config.
|
|
115
|
+
* @param {string|null|undefined} preset
|
|
116
|
+
* @param {string[]} errors - Mutated: push any validation errors
|
|
117
|
+
*/
|
|
118
|
+
function validatePreset(preset, errors) {
|
|
119
|
+
if (preset == null) return;
|
|
120
|
+
if (typeof preset !== 'string' || !PRESETS[preset]) {
|
|
121
|
+
errors.push(
|
|
122
|
+
`boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${preset}")`,
|
|
123
|
+
);
|
|
106
124
|
}
|
|
125
|
+
}
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Validate a single rule's target list (`notTo` or `onlyTo`).
|
|
129
|
+
* @param {*} list - The target list value
|
|
130
|
+
* @param {string} field - "notTo" or "onlyTo"
|
|
131
|
+
* @param {number} idx - Rule index for error messages
|
|
132
|
+
* @param {Set<string>} moduleNames
|
|
133
|
+
* @param {string[]} errors - Mutated
|
|
134
|
+
*/
|
|
135
|
+
function validateTargetList(list, field, idx, moduleNames, errors) {
|
|
136
|
+
if (!Array.isArray(list)) {
|
|
137
|
+
errors.push(`boundaries.rules[${idx}]: "${field}" must be an array`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const target of list) {
|
|
141
|
+
if (!moduleNames.has(target)) {
|
|
142
|
+
errors.push(`boundaries.rules[${idx}]: "${field}" references unknown module "${target}"`);
|
|
120
143
|
}
|
|
121
144
|
}
|
|
145
|
+
}
|
|
122
146
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Validate the `rules` array of a boundary config.
|
|
149
|
+
* @param {Array} rules
|
|
150
|
+
* @param {object|undefined} modules - The modules config (for cross-referencing names)
|
|
151
|
+
* @param {string[]} errors - Mutated
|
|
152
|
+
*/
|
|
153
|
+
function validateRules(rules, modules, errors) {
|
|
154
|
+
if (!rules) return;
|
|
155
|
+
if (!Array.isArray(rules)) {
|
|
156
|
+
errors.push('boundaries.rules must be an array');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const moduleNames = modules ? new Set(Object.keys(modules)) : new Set();
|
|
160
|
+
for (let i = 0; i < rules.length; i++) {
|
|
161
|
+
const rule = rules[i];
|
|
162
|
+
if (!rule.from) {
|
|
163
|
+
errors.push(`boundaries.rules[${i}]: missing "from" field`);
|
|
164
|
+
} else if (!moduleNames.has(rule.from)) {
|
|
165
|
+
errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
|
|
166
|
+
}
|
|
167
|
+
if (rule.notTo && rule.onlyTo) {
|
|
168
|
+
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
|
|
169
|
+
}
|
|
170
|
+
if (!rule.notTo && !rule.onlyTo) {
|
|
171
|
+
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
|
|
129
172
|
}
|
|
173
|
+
if (rule.notTo) validateTargetList(rule.notTo, 'notTo', i, moduleNames, errors);
|
|
174
|
+
if (rule.onlyTo) validateTargetList(rule.onlyTo, 'onlyTo', i, moduleNames, errors);
|
|
130
175
|
}
|
|
176
|
+
}
|
|
131
177
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (rule.notTo && rule.onlyTo) {
|
|
146
|
-
errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
|
|
147
|
-
}
|
|
148
|
-
if (!rule.notTo && !rule.onlyTo) {
|
|
149
|
-
errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
|
|
150
|
-
}
|
|
151
|
-
if (rule.notTo) {
|
|
152
|
-
if (!Array.isArray(rule.notTo)) {
|
|
153
|
-
errors.push(`boundaries.rules[${i}]: "notTo" must be an array`);
|
|
154
|
-
} else {
|
|
155
|
-
for (const target of rule.notTo) {
|
|
156
|
-
if (!moduleNames.has(target)) {
|
|
157
|
-
errors.push(
|
|
158
|
-
`boundaries.rules[${i}]: "notTo" references unknown module "${target}"`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (rule.onlyTo) {
|
|
165
|
-
if (!Array.isArray(rule.onlyTo)) {
|
|
166
|
-
errors.push(`boundaries.rules[${i}]: "onlyTo" must be an array`);
|
|
167
|
-
} else {
|
|
168
|
-
for (const target of rule.onlyTo) {
|
|
169
|
-
if (!moduleNames.has(target)) {
|
|
170
|
-
errors.push(
|
|
171
|
-
`boundaries.rules[${i}]: "onlyTo" references unknown module "${target}"`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate that module layer assignments match preset layers.
|
|
180
|
+
* @param {object} config
|
|
181
|
+
* @param {string[]} errors - Mutated
|
|
182
|
+
*/
|
|
183
|
+
function validateLayerAssignments(config, errors) {
|
|
184
|
+
if (!config.preset || !PRESETS[config.preset] || !config.modules) return;
|
|
185
|
+
const presetLayers = new Set(PRESETS[config.preset].layers);
|
|
186
|
+
for (const [name, value] of Object.entries(config.modules)) {
|
|
187
|
+
if (typeof value === 'object' && value.layer && !presetLayers.has(value.layer)) {
|
|
188
|
+
errors.push(
|
|
189
|
+
`boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
|
|
190
|
+
);
|
|
178
191
|
}
|
|
179
192
|
}
|
|
193
|
+
}
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
195
|
+
/**
|
|
196
|
+
* Validate a boundary configuration object.
|
|
197
|
+
* @param {object} config - The `manifesto.boundaries` config
|
|
198
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
199
|
+
*/
|
|
200
|
+
export function validateBoundaryConfig(config) {
|
|
201
|
+
if (!config || typeof config !== 'object') {
|
|
202
|
+
return { valid: false, errors: ['boundaries config must be an object'] };
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
const errors = [];
|
|
206
|
+
validateModules(config.modules, errors);
|
|
207
|
+
validatePreset(config.preset, errors);
|
|
208
|
+
validateRules(config.rules, config.modules, errors);
|
|
209
|
+
validateLayerAssignments(config, errors);
|
|
195
210
|
return { valid: errors.length === 0, errors };
|
|
196
211
|
}
|
|
197
212
|
|
|
@@ -11,9 +11,9 @@ import fs from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import Database from 'better-sqlite3';
|
|
14
|
-
import { buildGraph } from '
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { buildGraph } from '../domain/graph/builder.js';
|
|
15
|
+
import { kindIcon } from '../domain/queries.js';
|
|
16
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
17
17
|
|
|
18
18
|
// ─── Git Helpers ────────────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { CFG_RULES } from '
|
|
10
|
+
import { CFG_RULES } from '../ast-analysis/rules/index.js';
|
|
11
11
|
import {
|
|
12
12
|
makeCfgRules as _makeCfgRules,
|
|
13
13
|
buildExtensionSet,
|
|
14
14
|
buildExtToLangMap,
|
|
15
|
-
} from '
|
|
16
|
-
import { walkWithVisitors } from '
|
|
17
|
-
import { createCfgVisitor } from '
|
|
15
|
+
} from '../ast-analysis/shared.js';
|
|
16
|
+
import { walkWithVisitors } from '../ast-analysis/visitor.js';
|
|
17
|
+
import { createCfgVisitor } from '../ast-analysis/visitors/cfg-visitor.js';
|
|
18
18
|
import {
|
|
19
19
|
deleteCfgForNode,
|
|
20
20
|
getCfgBlocks,
|
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
getFunctionNodeId,
|
|
23
23
|
hasCfgTables,
|
|
24
24
|
openReadonlyOrFail,
|
|
25
|
-
} from '
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
25
|
+
} from '../db/index.js';
|
|
26
|
+
import { debug, info } from '../infrastructure/logger.js';
|
|
27
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
28
|
+
import { findNodes } from './shared/find-nodes.js';
|
|
29
29
|
|
|
30
30
|
// Re-export for backward compatibility
|
|
31
31
|
export { _makeCfgRules as makeCfgRules, CFG_RULES };
|
|
@@ -68,30 +68,15 @@ export function buildFunctionCFG(functionNode, langId) {
|
|
|
68
68
|
return { blocks: r.blocks, edges: r.edges, cyclomatic: r.cyclomatic };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// ─── Build-Time
|
|
71
|
+
// ─── Build-Time Helpers ─────────────────────────────────────────────────
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
* Build CFG data for all function/method definitions and persist to DB.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} db - open better-sqlite3 database (read-write)
|
|
77
|
-
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId }>
|
|
78
|
-
* @param {string} rootDir - absolute project root path
|
|
79
|
-
* @param {object} [_engineOpts] - engine options (unused; always uses WASM for AST)
|
|
80
|
-
*/
|
|
81
|
-
export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
82
|
-
// Lazily init WASM parsers if needed
|
|
83
|
-
let parsers = null;
|
|
73
|
+
async function initCfgParsers(fileSymbols) {
|
|
84
74
|
let needsFallback = false;
|
|
85
75
|
|
|
86
|
-
// Always build ext→langId map so native-only builds (where _langId is unset)
|
|
87
|
-
// can still derive the language from the file extension.
|
|
88
|
-
const extToLang = buildExtToLangMap();
|
|
89
|
-
|
|
90
76
|
for (const [relPath, symbols] of fileSymbols) {
|
|
91
77
|
if (!symbols._tree) {
|
|
92
78
|
const ext = path.extname(relPath).toLowerCase();
|
|
93
79
|
if (CFG_EXTENSIONS.has(ext)) {
|
|
94
|
-
// Check if all function/method defs already have native CFG data
|
|
95
80
|
const hasNativeCfg = symbols.definitions
|
|
96
81
|
.filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
|
|
97
82
|
.every((d) => d.cfg === null || d.cfg?.blocks?.length);
|
|
@@ -103,18 +88,131 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
103
88
|
}
|
|
104
89
|
}
|
|
105
90
|
|
|
91
|
+
let parsers = null;
|
|
92
|
+
let getParserFn = null;
|
|
93
|
+
|
|
106
94
|
if (needsFallback) {
|
|
107
|
-
const { createParsers } = await import('
|
|
95
|
+
const { createParsers } = await import('../domain/parser.js');
|
|
108
96
|
parsers = await createParsers();
|
|
97
|
+
const mod = await import('../domain/parser.js');
|
|
98
|
+
getParserFn = mod.getParser;
|
|
109
99
|
}
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
return { parsers, getParserFn };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn) {
|
|
105
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
106
|
+
let tree = symbols._tree;
|
|
107
|
+
let langId = symbols._langId;
|
|
108
|
+
|
|
109
|
+
const allNative = symbols.definitions
|
|
110
|
+
.filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
|
|
111
|
+
.every((d) => d.cfg === null || d.cfg?.blocks?.length);
|
|
112
|
+
|
|
113
|
+
if (!tree && !allNative) {
|
|
114
|
+
if (!getParserFn) return null;
|
|
115
|
+
langId = extToLang.get(ext);
|
|
116
|
+
if (!langId || !CFG_RULES.has(langId)) return null;
|
|
117
|
+
|
|
118
|
+
const absPath = path.join(rootDir, relPath);
|
|
119
|
+
let code;
|
|
120
|
+
try {
|
|
121
|
+
code = fs.readFileSync(absPath, 'utf-8');
|
|
122
|
+
} catch (e) {
|
|
123
|
+
debug(`cfg: cannot read ${relPath}: ${e.message}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parser = getParserFn(parsers, absPath);
|
|
128
|
+
if (!parser) return null;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
tree = parser.parse(code);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
debug(`cfg: parse failed for ${relPath}: ${e.message}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!langId) {
|
|
139
|
+
langId = extToLang.get(ext);
|
|
140
|
+
if (!langId) return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { tree, langId };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildVisitorCfgMap(tree, cfgRules, symbols, langId) {
|
|
147
|
+
const needsVisitor =
|
|
148
|
+
tree &&
|
|
149
|
+
symbols.definitions.some(
|
|
150
|
+
(d) =>
|
|
151
|
+
(d.kind === 'function' || d.kind === 'method') &&
|
|
152
|
+
d.line &&
|
|
153
|
+
d.cfg !== null &&
|
|
154
|
+
!d.cfg?.blocks?.length,
|
|
155
|
+
);
|
|
156
|
+
if (!needsVisitor) return null;
|
|
157
|
+
|
|
158
|
+
const visitor = createCfgVisitor(cfgRules);
|
|
159
|
+
const walkerOpts = {
|
|
160
|
+
functionNodeTypes: new Set(cfgRules.functionNodes),
|
|
161
|
+
nestingNodeTypes: new Set(),
|
|
162
|
+
getFunctionName: (node) => {
|
|
163
|
+
const nameNode = node.childForFieldName('name');
|
|
164
|
+
return nameNode ? nameNode.text : null;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
|
|
168
|
+
const cfgResults = walkResults.cfg || [];
|
|
169
|
+
const visitorCfgByLine = new Map();
|
|
170
|
+
for (const r of cfgResults) {
|
|
171
|
+
if (r.funcNode) {
|
|
172
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
173
|
+
if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
|
|
174
|
+
visitorCfgByLine.get(line).push(r);
|
|
175
|
+
}
|
|
115
176
|
}
|
|
177
|
+
return visitorCfgByLine;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function persistCfg(cfg, nodeId, insertBlock, insertEdge) {
|
|
181
|
+
const blockDbIds = new Map();
|
|
182
|
+
for (const block of cfg.blocks) {
|
|
183
|
+
const result = insertBlock.run(
|
|
184
|
+
nodeId,
|
|
185
|
+
block.index,
|
|
186
|
+
block.type,
|
|
187
|
+
block.startLine,
|
|
188
|
+
block.endLine,
|
|
189
|
+
block.label,
|
|
190
|
+
);
|
|
191
|
+
blockDbIds.set(block.index, result.lastInsertRowid);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const edge of cfg.edges) {
|
|
195
|
+
const sourceDbId = blockDbIds.get(edge.sourceIndex);
|
|
196
|
+
const targetDbId = blockDbIds.get(edge.targetIndex);
|
|
197
|
+
if (sourceDbId && targetDbId) {
|
|
198
|
+
insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
116
202
|
|
|
117
|
-
|
|
203
|
+
// ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build CFG data for all function/method definitions and persist to DB.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} db - open better-sqlite3 database (read-write)
|
|
209
|
+
* @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId }>
|
|
210
|
+
* @param {string} rootDir - absolute project root path
|
|
211
|
+
* @param {object} [_engineOpts] - engine options (unused; always uses WASM for AST)
|
|
212
|
+
*/
|
|
213
|
+
export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
214
|
+
const extToLang = buildExtToLangMap();
|
|
215
|
+
const { parsers, getParserFn } = await initCfgParsers(fileSymbols);
|
|
118
216
|
|
|
119
217
|
const insertBlock = db.prepare(
|
|
120
218
|
`INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
|
|
@@ -131,79 +229,14 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
131
229
|
const ext = path.extname(relPath).toLowerCase();
|
|
132
230
|
if (!CFG_EXTENSIONS.has(ext)) continue;
|
|
133
231
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Check if all defs already have native CFG — skip WASM parse if so
|
|
138
|
-
const allNative = symbols.definitions
|
|
139
|
-
.filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
|
|
140
|
-
.every((d) => d.cfg === null || d.cfg?.blocks?.length);
|
|
141
|
-
|
|
142
|
-
// WASM fallback if no cached tree and not all native
|
|
143
|
-
if (!tree && !allNative) {
|
|
144
|
-
if (!getParserFn) continue;
|
|
145
|
-
langId = extToLang.get(ext);
|
|
146
|
-
if (!langId || !CFG_RULES.has(langId)) continue;
|
|
147
|
-
|
|
148
|
-
const absPath = path.join(rootDir, relPath);
|
|
149
|
-
let code;
|
|
150
|
-
try {
|
|
151
|
-
code = fs.readFileSync(absPath, 'utf-8');
|
|
152
|
-
} catch {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const parser = getParserFn(parsers, absPath);
|
|
157
|
-
if (!parser) continue;
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
tree = parser.parse(code);
|
|
161
|
-
} catch {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (!langId) {
|
|
167
|
-
langId = extToLang.get(ext);
|
|
168
|
-
if (!langId) continue;
|
|
169
|
-
}
|
|
232
|
+
const treeLang = getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
|
|
233
|
+
if (!treeLang) continue;
|
|
234
|
+
const { tree, langId } = treeLang;
|
|
170
235
|
|
|
171
236
|
const cfgRules = CFG_RULES.get(langId);
|
|
172
237
|
if (!cfgRules) continue;
|
|
173
238
|
|
|
174
|
-
|
|
175
|
-
// that don't already have pre-computed data (from native engine or unified walk)
|
|
176
|
-
let visitorCfgByLine = null;
|
|
177
|
-
const needsVisitor =
|
|
178
|
-
tree &&
|
|
179
|
-
symbols.definitions.some(
|
|
180
|
-
(d) =>
|
|
181
|
-
(d.kind === 'function' || d.kind === 'method') &&
|
|
182
|
-
d.line &&
|
|
183
|
-
d.cfg !== null &&
|
|
184
|
-
!d.cfg?.blocks?.length,
|
|
185
|
-
);
|
|
186
|
-
if (needsVisitor) {
|
|
187
|
-
const visitor = createCfgVisitor(cfgRules);
|
|
188
|
-
const walkerOpts = {
|
|
189
|
-
functionNodeTypes: new Set(cfgRules.functionNodes),
|
|
190
|
-
nestingNodeTypes: new Set(),
|
|
191
|
-
getFunctionName: (node) => {
|
|
192
|
-
const nameNode = node.childForFieldName('name');
|
|
193
|
-
return nameNode ? nameNode.text : null;
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
|
|
197
|
-
const cfgResults = walkResults.cfg || [];
|
|
198
|
-
visitorCfgByLine = new Map();
|
|
199
|
-
for (const r of cfgResults) {
|
|
200
|
-
if (r.funcNode) {
|
|
201
|
-
const line = r.funcNode.startPosition.row + 1;
|
|
202
|
-
if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
|
|
203
|
-
visitorCfgByLine.get(line).push(r);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
239
|
+
const visitorCfgByLine = buildVisitorCfgMap(tree, cfgRules, symbols, langId);
|
|
207
240
|
|
|
208
241
|
for (const def of symbols.definitions) {
|
|
209
242
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
@@ -212,7 +245,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
212
245
|
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
213
246
|
if (!nodeId) continue;
|
|
214
247
|
|
|
215
|
-
// Use pre-computed CFG (native engine or unified walk), then visitor fallback
|
|
216
248
|
let cfg = null;
|
|
217
249
|
if (def.cfg?.blocks?.length) {
|
|
218
250
|
cfg = def.cfg;
|
|
@@ -231,36 +263,10 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
231
263
|
|
|
232
264
|
if (!cfg || cfg.blocks.length === 0) continue;
|
|
233
265
|
|
|
234
|
-
// Clear old CFG data for this function
|
|
235
266
|
deleteCfgForNode(db, nodeId);
|
|
236
|
-
|
|
237
|
-
// Insert blocks and build index→dbId mapping
|
|
238
|
-
const blockDbIds = new Map();
|
|
239
|
-
for (const block of cfg.blocks) {
|
|
240
|
-
const result = insertBlock.run(
|
|
241
|
-
nodeId,
|
|
242
|
-
block.index,
|
|
243
|
-
block.type,
|
|
244
|
-
block.startLine,
|
|
245
|
-
block.endLine,
|
|
246
|
-
block.label,
|
|
247
|
-
);
|
|
248
|
-
blockDbIds.set(block.index, result.lastInsertRowid);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Insert edges
|
|
252
|
-
for (const edge of cfg.edges) {
|
|
253
|
-
const sourceDbId = blockDbIds.get(edge.sourceIndex);
|
|
254
|
-
const targetDbId = blockDbIds.get(edge.targetIndex);
|
|
255
|
-
if (sourceDbId && targetDbId) {
|
|
256
|
-
insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
267
|
+
persistCfg(cfg, nodeId, insertBlock, insertEdge);
|
|
260
268
|
analyzed++;
|
|
261
269
|
}
|
|
262
|
-
|
|
263
|
-
// Don't release _tree here — complexity/dataflow may still need it
|
|
264
270
|
}
|
|
265
271
|
});
|
|
266
272
|
|
|
@@ -273,27 +279,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
273
279
|
|
|
274
280
|
// ─── Query-Time Functions ───────────────────────────────────────────────
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
const kinds = opts.kind ? [opts.kind] : ['function', 'method'];
|
|
278
|
-
const placeholders = kinds.map(() => '?').join(', ');
|
|
279
|
-
const params = [`%${name}%`, ...kinds];
|
|
280
|
-
|
|
281
|
-
let fileCondition = '';
|
|
282
|
-
if (opts.file) {
|
|
283
|
-
fileCondition = ' AND n.file LIKE ?';
|
|
284
|
-
params.push(`%${opts.file}%`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const rows = db
|
|
288
|
-
.prepare(
|
|
289
|
-
`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
|
|
290
|
-
FROM nodes n
|
|
291
|
-
WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`,
|
|
292
|
-
)
|
|
293
|
-
.all(...params);
|
|
294
|
-
|
|
295
|
-
return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
296
|
-
}
|
|
282
|
+
const CFG_DEFAULT_KINDS = ['function', 'method'];
|
|
297
283
|
|
|
298
284
|
/**
|
|
299
285
|
* Load CFG data for a function from the database.
|
|
@@ -317,7 +303,12 @@ export function cfgData(name, customDbPath, opts = {}) {
|
|
|
317
303
|
};
|
|
318
304
|
}
|
|
319
305
|
|
|
320
|
-
const nodes = findNodes(
|
|
306
|
+
const nodes = findNodes(
|
|
307
|
+
db,
|
|
308
|
+
name,
|
|
309
|
+
{ noTests, file: opts.file, kind: opts.kind },
|
|
310
|
+
CFG_DEFAULT_KINDS,
|
|
311
|
+
);
|
|
321
312
|
if (nodes.length === 0) {
|
|
322
313
|
return { name, results: [] };
|
|
323
314
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { findDbPath, openReadonlyOrFail } from '../db/index.js';
|
|
5
|
+
import { bfsTransitiveCallers } from '../domain/analysis/impact.js';
|
|
6
|
+
import { findCycles } from '../domain/graph/cycles.js';
|
|
7
|
+
import { loadConfig } from '../infrastructure/config.js';
|
|
8
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
8
9
|
import { matchOwners, parseCodeowners } from './owners.js';
|
|
9
10
|
|
|
10
11
|
// ─── Diff Parser ──────────────────────────────────────────────────────
|
|
@@ -96,31 +97,10 @@ export function checkMaxBlastRadius(db, changedRanges, threshold, noTests, maxDe
|
|
|
96
97
|
}
|
|
97
98
|
if (!overlaps) continue;
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (let d = 1; d <= maxDepth; d++) {
|
|
104
|
-
const nextFrontier = [];
|
|
105
|
-
for (const fid of frontier) {
|
|
106
|
-
const callers = db
|
|
107
|
-
.prepare(
|
|
108
|
-
`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
|
|
109
|
-
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
110
|
-
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
111
|
-
)
|
|
112
|
-
.all(fid);
|
|
113
|
-
for (const c of callers) {
|
|
114
|
-
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
115
|
-
visited.add(c.id);
|
|
116
|
-
nextFrontier.push(c.id);
|
|
117
|
-
totalCallers++;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
frontier = nextFrontier;
|
|
122
|
-
if (frontier.length === 0) break;
|
|
123
|
-
}
|
|
100
|
+
const { totalDependents: totalCallers } = bfsTransitiveCallers(db, def.id, {
|
|
101
|
+
noTests,
|
|
102
|
+
maxDepth,
|
|
103
|
+
});
|
|
124
104
|
|
|
125
105
|
if (totalCallers > maxFound) maxFound = totalCallers;
|
|
126
106
|
if (totalCallers > threshold) {
|
|
@@ -240,7 +220,10 @@ export function checkData(customDbPath, opts = {}) {
|
|
|
240
220
|
const maxDepth = opts.depth || 3;
|
|
241
221
|
|
|
242
222
|
// Load config defaults for check predicates
|
|
243
|
-
|
|
223
|
+
// NOTE: opts.config is loaded from process.cwd() at startup (via CLI context),
|
|
224
|
+
// which may differ from the DB's parent repo root when --db points to an external
|
|
225
|
+
// project. This is an acceptable trade-off to avoid duplicate I/O on the hot path.
|
|
226
|
+
const config = opts.config || loadConfig(repoRoot);
|
|
244
227
|
const checkConfig = config.check || {};
|
|
245
228
|
|
|
246
229
|
// Resolve which predicates are enabled: CLI flags ?? config ?? built-in defaults
|