@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
|
@@ -21,254 +21,280 @@ import {
|
|
|
21
21
|
truncate,
|
|
22
22
|
} from '../visitor-utils.js';
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
* Create a dataflow visitor for use with walkWithVisitors.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} rules - DATAFLOW_RULES for the language
|
|
28
|
-
* @returns {Visitor}
|
|
29
|
-
*/
|
|
30
|
-
export function createDataflowVisitor(rules) {
|
|
31
|
-
const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
|
|
32
|
-
|
|
33
|
-
const parameters = [];
|
|
34
|
-
const returns = [];
|
|
35
|
-
const assignments = [];
|
|
36
|
-
const argFlows = [];
|
|
37
|
-
const mutations = [];
|
|
24
|
+
// ── Scope helpers ───────────────────────────────────────────────────────
|
|
38
25
|
|
|
39
|
-
|
|
26
|
+
function currentScope(scopeStack) {
|
|
27
|
+
return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
28
|
+
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
function findBinding(name, scopeStack) {
|
|
31
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
32
|
+
const scope = scopeStack[i];
|
|
33
|
+
if (scope.params.has(name))
|
|
34
|
+
return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
|
|
35
|
+
if (scope.locals.has(name))
|
|
36
|
+
return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
|
|
43
37
|
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
41
|
+
function bindingConfidence(binding) {
|
|
42
|
+
if (!binding) return 0.5;
|
|
43
|
+
if (binding.type === 'param') return 1.0;
|
|
44
|
+
if (binding.type === 'local') {
|
|
45
|
+
if (binding.source?.type === 'call_return') return 0.9;
|
|
46
|
+
if (binding.source?.type === 'destructured') return 0.8;
|
|
47
|
+
return 0.9;
|
|
54
48
|
}
|
|
49
|
+
return 0.5;
|
|
50
|
+
}
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
if (!binding) return 0.5;
|
|
58
|
-
if (binding.type === 'param') return 1.0;
|
|
59
|
-
if (binding.type === 'local') {
|
|
60
|
-
if (binding.source?.type === 'call_return') return 0.9;
|
|
61
|
-
if (binding.source?.type === 'destructured') return 0.8;
|
|
62
|
-
return 0.9;
|
|
63
|
-
}
|
|
64
|
-
return 0.5;
|
|
65
|
-
}
|
|
52
|
+
// ── Node helpers ────────────────────────────────────────────────────────
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
return node;
|
|
54
|
+
function unwrapAwait(node, rules) {
|
|
55
|
+
if (rules.awaitNode && node.type === rules.awaitNode) {
|
|
56
|
+
return node.namedChildren[0] || node;
|
|
72
57
|
}
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
function isCall(node, isCallNode) {
|
|
62
|
+
return node && isCallNode(node.type);
|
|
63
|
+
}
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
let nameNode = node.childForFieldName(rules.varNameField);
|
|
80
|
-
let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
|
|
65
|
+
// ── Node handlers ───────────────────────────────────────────────────────
|
|
81
66
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
valueNode = child.childForFieldName('value') || child.namedChildren[0];
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
67
|
+
function handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode) {
|
|
68
|
+
let nameNode = node.childForFieldName(rules.varNameField);
|
|
69
|
+
let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
|
|
90
70
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
71
|
+
if (!valueNode && rules.equalsClauseType) {
|
|
72
|
+
for (const child of node.namedChildren) {
|
|
73
|
+
if (child.type === rules.equalsClauseType) {
|
|
74
|
+
valueNode = child.childForFieldName('value') || child.namedChildren[0];
|
|
75
|
+
break;
|
|
97
76
|
}
|
|
98
77
|
}
|
|
78
|
+
}
|
|
99
79
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
80
|
+
if (!valueNode) {
|
|
81
|
+
for (const child of node.namedChildren) {
|
|
82
|
+
if (child !== nameNode && isCall(unwrapAwait(child, rules), isCallNode)) {
|
|
83
|
+
valueNode = child;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
103
86
|
}
|
|
87
|
+
}
|
|
104
88
|
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const callExpr = isCall(unwrapped) ? unwrapped : null;
|
|
89
|
+
if (rules.expressionListType) {
|
|
90
|
+
if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
|
|
91
|
+
if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
|
|
92
|
+
}
|
|
110
93
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
scope.locals.set(n, { type: 'destructured', callee });
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
const varName =
|
|
131
|
-
nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
|
|
132
|
-
? nameNode.text
|
|
133
|
-
: nameNode.text;
|
|
94
|
+
const scope = currentScope(scopeStack);
|
|
95
|
+
if (!nameNode || !valueNode || !scope) return;
|
|
96
|
+
|
|
97
|
+
const unwrapped = unwrapAwait(valueNode, rules);
|
|
98
|
+
const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
|
|
99
|
+
|
|
100
|
+
if (callExpr) {
|
|
101
|
+
const callee = resolveCalleeName(callExpr, rules);
|
|
102
|
+
if (callee && scope.funcName) {
|
|
103
|
+
if (
|
|
104
|
+
(rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
|
|
105
|
+
(rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
|
|
106
|
+
) {
|
|
107
|
+
const names = extractParamNames(nameNode, rules);
|
|
108
|
+
for (const n of names) {
|
|
134
109
|
assignments.push({
|
|
135
|
-
varName,
|
|
110
|
+
varName: n,
|
|
136
111
|
callerFunc: scope.funcName,
|
|
137
112
|
sourceCallName: callee,
|
|
138
113
|
expression: truncate(node.text),
|
|
139
114
|
line: node.startPosition.row + 1,
|
|
140
115
|
});
|
|
141
|
-
scope.locals.set(
|
|
116
|
+
scope.locals.set(n, { type: 'destructured', callee });
|
|
142
117
|
}
|
|
118
|
+
} else {
|
|
119
|
+
const varName =
|
|
120
|
+
nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
|
|
121
|
+
? nameNode.text
|
|
122
|
+
: nameNode.text;
|
|
123
|
+
assignments.push({
|
|
124
|
+
varName,
|
|
125
|
+
callerFunc: scope.funcName,
|
|
126
|
+
sourceCallName: callee,
|
|
127
|
+
expression: truncate(node.text),
|
|
128
|
+
line: node.startPosition.row + 1,
|
|
129
|
+
});
|
|
130
|
+
scope.locals.set(varName, { type: 'call_return', callee });
|
|
143
131
|
}
|
|
144
132
|
}
|
|
145
133
|
}
|
|
134
|
+
}
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
136
|
+
function handleAssignment(node, rules, scopeStack, assignments, mutations, isCallNode) {
|
|
137
|
+
const left = node.childForFieldName(rules.assignLeftField);
|
|
138
|
+
const right = node.childForFieldName(rules.assignRightField);
|
|
139
|
+
const scope = currentScope(scopeStack);
|
|
140
|
+
if (!scope?.funcName) return;
|
|
141
|
+
|
|
142
|
+
if (left && rules.memberNode && left.type === rules.memberNode) {
|
|
143
|
+
const receiver = memberReceiver(left, rules);
|
|
144
|
+
if (receiver) {
|
|
145
|
+
const binding = findBinding(receiver, scopeStack);
|
|
146
|
+
if (binding) {
|
|
147
|
+
mutations.push({
|
|
148
|
+
funcName: scope.funcName,
|
|
149
|
+
receiverName: receiver,
|
|
150
|
+
binding,
|
|
151
|
+
mutatingExpr: truncate(node.text),
|
|
152
|
+
line: node.startPosition.row + 1,
|
|
153
|
+
});
|
|
166
154
|
}
|
|
167
155
|
}
|
|
156
|
+
}
|
|
168
157
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
158
|
+
if (left && isIdent(left.type, rules) && right) {
|
|
159
|
+
const unwrapped = unwrapAwait(right, rules);
|
|
160
|
+
const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
|
|
161
|
+
if (callExpr) {
|
|
162
|
+
const callee = resolveCalleeName(callExpr, rules);
|
|
163
|
+
if (callee) {
|
|
164
|
+
assignments.push({
|
|
165
|
+
varName: left.text,
|
|
166
|
+
callerFunc: scope.funcName,
|
|
167
|
+
sourceCallName: callee,
|
|
168
|
+
expression: truncate(node.text),
|
|
169
|
+
line: node.startPosition.row + 1,
|
|
170
|
+
});
|
|
171
|
+
scope.locals.set(left.text, { type: 'call_return', callee });
|
|
184
172
|
}
|
|
185
173
|
}
|
|
186
174
|
}
|
|
175
|
+
}
|
|
187
176
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
177
|
+
function handleCallExpr(node, rules, scopeStack, argFlows) {
|
|
178
|
+
const callee = resolveCalleeName(node, rules);
|
|
179
|
+
const argsNode = node.childForFieldName(rules.callArgsField);
|
|
180
|
+
const scope = currentScope(scopeStack);
|
|
181
|
+
if (!callee || !argsNode || !scope?.funcName) return;
|
|
193
182
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
183
|
+
let argIndex = 0;
|
|
184
|
+
for (let arg of argsNode.namedChildren) {
|
|
185
|
+
if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
|
|
186
|
+
arg = arg.namedChildren[0] || arg;
|
|
187
|
+
}
|
|
188
|
+
const unwrapped =
|
|
189
|
+
rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
|
|
190
|
+
if (!unwrapped) {
|
|
191
|
+
argIndex++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
205
194
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
195
|
+
const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
|
|
196
|
+
const argMember =
|
|
197
|
+
rules.memberNode && unwrapped.type === rules.memberNode
|
|
198
|
+
? memberReceiver(unwrapped, rules)
|
|
199
|
+
: null;
|
|
200
|
+
const trackedName = argName || argMember;
|
|
201
|
+
|
|
202
|
+
if (trackedName) {
|
|
203
|
+
const binding = findBinding(trackedName, scopeStack);
|
|
204
|
+
if (binding) {
|
|
205
|
+
argFlows.push({
|
|
206
|
+
callerFunc: scope.funcName,
|
|
207
|
+
calleeName: callee,
|
|
208
|
+
argIndex,
|
|
209
|
+
argName: trackedName,
|
|
210
|
+
binding,
|
|
211
|
+
confidence: bindingConfidence(binding),
|
|
212
|
+
expression: truncate(arg.text),
|
|
213
|
+
line: node.startPosition.row + 1,
|
|
214
|
+
});
|
|
227
215
|
}
|
|
228
|
-
argIndex++;
|
|
229
216
|
}
|
|
217
|
+
argIndex++;
|
|
230
218
|
}
|
|
219
|
+
}
|
|
231
220
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
221
|
+
function handleExprStmtMutation(node, rules, scopeStack, mutations, isCallNode) {
|
|
222
|
+
if (rules.mutatingMethods.size === 0) return;
|
|
223
|
+
const expr = node.namedChildren[0];
|
|
224
|
+
if (!expr || !isCall(expr, isCallNode)) return;
|
|
236
225
|
|
|
237
|
-
|
|
238
|
-
|
|
226
|
+
let methodName = null;
|
|
227
|
+
let receiver = null;
|
|
239
228
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
229
|
+
const fn = expr.childForFieldName(rules.callFunctionField);
|
|
230
|
+
if (fn && fn.type === rules.memberNode) {
|
|
231
|
+
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
232
|
+
methodName = prop ? prop.text : null;
|
|
233
|
+
receiver = memberReceiver(fn, rules);
|
|
234
|
+
}
|
|
246
235
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
236
|
+
if (!receiver && rules.callObjectField) {
|
|
237
|
+
const obj = expr.childForFieldName(rules.callObjectField);
|
|
238
|
+
const name = expr.childForFieldName(rules.callFunctionField);
|
|
239
|
+
if (obj && name) {
|
|
240
|
+
methodName = name.text;
|
|
241
|
+
receiver = isIdent(obj.type, rules) ? obj.text : null;
|
|
254
242
|
}
|
|
243
|
+
}
|
|
255
244
|
|
|
256
|
-
|
|
245
|
+
if (!methodName || !rules.mutatingMethods.has(methodName)) return;
|
|
257
246
|
|
|
258
|
-
|
|
259
|
-
|
|
247
|
+
const scope = currentScope(scopeStack);
|
|
248
|
+
if (!receiver || !scope?.funcName) return;
|
|
260
249
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
250
|
+
const binding = findBinding(receiver, scopeStack);
|
|
251
|
+
if (binding) {
|
|
252
|
+
mutations.push({
|
|
253
|
+
funcName: scope.funcName,
|
|
254
|
+
receiverName: receiver,
|
|
255
|
+
binding,
|
|
256
|
+
mutatingExpr: truncate(expr.text),
|
|
257
|
+
line: node.startPosition.row + 1,
|
|
258
|
+
});
|
|
271
259
|
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Return statement handler ────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function handleReturn(node, rules, scopeStack, returns) {
|
|
265
|
+
if (node.parent?.type === rules.returnNode) return; // keyword token, not statement
|
|
266
|
+
|
|
267
|
+
const scope = currentScope(scopeStack);
|
|
268
|
+
if (scope?.funcName) {
|
|
269
|
+
const expr = node.namedChildren[0];
|
|
270
|
+
const referencedNames = [];
|
|
271
|
+
if (expr) collectIdentifiers(expr, referencedNames, rules);
|
|
272
|
+
returns.push({
|
|
273
|
+
funcName: scope.funcName,
|
|
274
|
+
expression: truncate(expr ? expr.text : ''),
|
|
275
|
+
referencedNames,
|
|
276
|
+
line: node.startPosition.row + 1,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Visitor factory ─────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a dataflow visitor for use with walkWithVisitors.
|
|
285
|
+
*
|
|
286
|
+
* @param {object} rules - DATAFLOW_RULES for the language
|
|
287
|
+
* @returns {Visitor}
|
|
288
|
+
*/
|
|
289
|
+
export function createDataflowVisitor(rules) {
|
|
290
|
+
const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
|
|
291
|
+
|
|
292
|
+
const parameters = [];
|
|
293
|
+
const returns = [];
|
|
294
|
+
const assignments = [];
|
|
295
|
+
const argFlows = [];
|
|
296
|
+
const mutations = [];
|
|
297
|
+
const scopeStack = [];
|
|
272
298
|
|
|
273
299
|
return {
|
|
274
300
|
name: 'dataflow',
|
|
@@ -300,54 +326,34 @@ export function createDataflowVisitor(rules) {
|
|
|
300
326
|
enterNode(node, _context) {
|
|
301
327
|
const t = node.type;
|
|
302
328
|
|
|
303
|
-
// Skip function nodes — handled by enterFunction/exitFunction
|
|
304
329
|
if (rules.functionNodes.has(t)) return;
|
|
305
330
|
|
|
306
|
-
// Return statements (skip keyword tokens inside return statements, e.g. Ruby's
|
|
307
|
-
// `return` node nests a `return` keyword child with the same type string)
|
|
308
331
|
if (rules.returnNode && t === rules.returnNode) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const scope = currentScope();
|
|
312
|
-
if (scope?.funcName) {
|
|
313
|
-
const expr = node.namedChildren[0];
|
|
314
|
-
const referencedNames = [];
|
|
315
|
-
if (expr) collectIdentifiers(expr, referencedNames, rules);
|
|
316
|
-
returns.push({
|
|
317
|
-
funcName: scope.funcName,
|
|
318
|
-
expression: truncate(expr ? expr.text : ''),
|
|
319
|
-
referencedNames,
|
|
320
|
-
line: node.startPosition.row + 1,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
332
|
+
handleReturn(node, rules, scopeStack, returns);
|
|
323
333
|
return;
|
|
324
334
|
}
|
|
325
335
|
|
|
326
|
-
// Variable declarations
|
|
327
336
|
if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
|
|
328
|
-
handleVarDeclarator(node);
|
|
337
|
+
handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
|
|
329
338
|
return;
|
|
330
339
|
}
|
|
331
340
|
if (rules.varDeclaratorNodes?.has(t)) {
|
|
332
|
-
handleVarDeclarator(node);
|
|
341
|
+
handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
|
|
333
342
|
return;
|
|
334
343
|
}
|
|
335
344
|
|
|
336
|
-
// Call expressions
|
|
337
345
|
if (isCallNode(t)) {
|
|
338
|
-
handleCallExpr(node);
|
|
346
|
+
handleCallExpr(node, rules, scopeStack, argFlows);
|
|
339
347
|
return;
|
|
340
348
|
}
|
|
341
349
|
|
|
342
|
-
// Assignment expressions
|
|
343
350
|
if (rules.assignmentNode && t === rules.assignmentNode) {
|
|
344
|
-
handleAssignment(node);
|
|
351
|
+
handleAssignment(node, rules, scopeStack, assignments, mutations, isCallNode);
|
|
345
352
|
return;
|
|
346
353
|
}
|
|
347
354
|
|
|
348
|
-
// Mutation detection via expression_statement
|
|
349
355
|
if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
|
|
350
|
-
handleExprStmtMutation(node);
|
|
356
|
+
handleExprStmtMutation(node, rules, scopeStack, mutations, isCallNode);
|
|
351
357
|
}
|
|
352
358
|
},
|
|
353
359
|
|
package/src/cli/commands/ast.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
2
|
+
import { ConfigError } from '../../shared/errors.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
4
5
|
name: 'ast [pattern]',
|
|
@@ -6,7 +7,7 @@ export const command = {
|
|
|
6
7
|
queryOpts: true,
|
|
7
8
|
options: [
|
|
8
9
|
['-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)'],
|
|
9
|
-
['-f, --file <path>', 'Scope to file (partial match)'],
|
|
10
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
10
11
|
],
|
|
11
12
|
async execute([pattern], opts, ctx) {
|
|
12
13
|
const { AST_NODE_KINDS, astQuery } = await import('../../ast.js');
|
|
@@ -16,11 +17,7 @@ export const command = {
|
|
|
16
17
|
astQuery(pattern, opts.db, {
|
|
17
18
|
kind: opts.kind,
|
|
18
19
|
file: opts.file,
|
|
19
|
-
|
|
20
|
-
json: opts.json,
|
|
21
|
-
ndjson: opts.ndjson,
|
|
22
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
23
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
20
|
+
...ctx.resolveQueryOpts(opts),
|
|
24
21
|
});
|
|
25
22
|
},
|
|
26
23
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EVERY_SYMBOL_KIND } from '../../queries.js';
|
|
3
|
-
import {
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
2
|
+
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
3
|
+
import { audit } from '../../presentation/audit.js';
|
|
4
|
+
import { explain } from '../../presentation/queries-cli.js';
|
|
5
|
+
import { config } from '../shared/options.js';
|
|
4
6
|
|
|
5
7
|
export const command = {
|
|
6
8
|
name: 'audit <target>',
|
|
@@ -9,7 +11,7 @@ export const command = {
|
|
|
9
11
|
['-d, --db <path>', 'Path to graph.db'],
|
|
10
12
|
['--quick', 'Structural summary only (skip impact analysis and health metrics)'],
|
|
11
13
|
['--depth <n>', 'Impact/explain depth', '3'],
|
|
12
|
-
['-f, --file <path>', 'Scope to file (partial match)'],
|
|
14
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
13
15
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
14
16
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
15
17
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -24,14 +26,11 @@ export const command = {
|
|
|
24
26
|
}
|
|
25
27
|
},
|
|
26
28
|
execute([target], opts, ctx) {
|
|
29
|
+
const qOpts = ctx.resolveQueryOpts(opts);
|
|
27
30
|
if (opts.quick) {
|
|
28
31
|
explain(target, opts.db, {
|
|
29
32
|
depth: parseInt(opts.depth, 10),
|
|
30
|
-
|
|
31
|
-
json: opts.json,
|
|
32
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
33
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
34
|
-
ndjson: opts.ndjson,
|
|
33
|
+
...qOpts,
|
|
35
34
|
});
|
|
36
35
|
return;
|
|
37
36
|
}
|
|
@@ -39,8 +38,9 @@ export const command = {
|
|
|
39
38
|
depth: parseInt(opts.depth, 10),
|
|
40
39
|
file: opts.file,
|
|
41
40
|
kind: opts.kind,
|
|
42
|
-
noTests:
|
|
43
|
-
json:
|
|
41
|
+
noTests: qOpts.noTests,
|
|
42
|
+
json: qOpts.json,
|
|
43
|
+
config,
|
|
44
44
|
});
|
|
45
45
|
},
|
|
46
46
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
3
|
+
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
4
|
+
import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../features/batch.js';
|
|
5
|
+
import { batch } from '../../presentation/batch.js';
|
|
6
|
+
import { ConfigError } from '../../shared/errors.js';
|
|
6
7
|
|
|
7
8
|
export const command = {
|
|
8
9
|
name: 'batch <command> [targets...]',
|
|
@@ -12,7 +13,7 @@ export const command = {
|
|
|
12
13
|
['--from-file <path>', 'Read targets from file (JSON array or newline-delimited)'],
|
|
13
14
|
['--stdin', 'Read targets from stdin (JSON array)'],
|
|
14
15
|
['--depth <n>', 'Traversal depth passed to underlying command'],
|
|
15
|
-
['-f, --file <path>', 'Scope to file (partial match)'],
|
|
16
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
16
17
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
17
18
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
18
19
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -9,7 +9,7 @@ export const command = {
|
|
|
9
9
|
['-f, --format <format>', 'Output format: text, mermaid, json', 'text'],
|
|
10
10
|
],
|
|
11
11
|
async execute([base, target], opts, ctx) {
|
|
12
|
-
const { branchCompare } = await import('../../
|
|
12
|
+
const { branchCompare } = await import('../../presentation/branch-compare.js');
|
|
13
13
|
await branchCompare(base, target, {
|
|
14
14
|
engine: ctx.program.opts().engine,
|
|
15
15
|
depth: parseInt(opts.depth, 10),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { brief } from '../../presentation/brief.js';
|
|
2
|
+
|
|
3
|
+
export const command = {
|
|
4
|
+
name: 'brief <file>',
|
|
5
|
+
description: 'Token-efficient file summary: symbols with roles, caller counts, risk tier',
|
|
6
|
+
queryOpts: true,
|
|
7
|
+
execute([file], opts, ctx) {
|
|
8
|
+
brief(file, opts.db, {
|
|
9
|
+
...ctx.resolveQueryOpts(opts),
|
|
10
|
+
});
|
|
11
|
+
},
|
|
12
|
+
};
|