@optave/codegraph 3.10.0 → 3.11.1
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 +40 -33
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +77 -0
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/children.js +1 -1
- package/dist/cli/commands/children.js.map +1 -1
- package/dist/cli/commands/diff-impact.js +1 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/cli/commands/roles.js +1 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/cli/commands/structure.js +1 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/cli/shared/options.js +1 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -6
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +148 -99
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +23 -637
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +141 -98
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +28 -20
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +153 -80
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +18 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +72 -4
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +209 -137
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/c.js +25 -6
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/cpp.js +47 -6
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.js +90 -14
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +108 -4
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/erlang.js +56 -20
- package/dist/extractors/erlang.js.map +1 -1
- package/dist/extractors/fsharp.d.ts +7 -0
- package/dist/extractors/fsharp.d.ts.map +1 -1
- package/dist/extractors/fsharp.js +94 -0
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +29 -33
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/groovy.js +41 -1
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/haskell.js +48 -4
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +198 -74
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/kotlin.js +4 -0
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +184 -47
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/python.js +7 -4
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +103 -87
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/scala.d.ts.map +1 -1
- package/dist/extractors/scala.js +18 -32
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +55 -69
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/verilog.js +80 -15
- package/dist/extractors/verilog.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.js +28 -36
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +4 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -0
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.js +1 -0
- package/dist/mcp/tools/semantic-search.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +10 -10
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/rules/index.ts +87 -0
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/children.ts +1 -1
- package/src/cli/commands/diff-impact.ts +1 -1
- package/src/cli/commands/embed.ts +54 -4
- package/src/cli/commands/roles.ts +1 -1
- package/src/cli/commands/structure.ts +1 -1
- package/src/cli/shared/options.ts +1 -1
- package/src/db/connection.ts +8 -0
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +223 -131
- package/src/domain/graph/builder/pipeline.ts +32 -785
- package/src/domain/graph/builder/stages/build-edges.ts +207 -142
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +29 -25
- package/src/domain/parser.ts +170 -67
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +75 -4
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +243 -153
- package/src/extractors/c.ts +27 -8
- package/src/extractors/cpp.ts +50 -8
- package/src/extractors/cuda.ts +90 -16
- package/src/extractors/elixir.ts +103 -4
- package/src/extractors/erlang.ts +63 -20
- package/src/extractors/fsharp.ts +104 -0
- package/src/extractors/gleam.ts +40 -39
- package/src/extractors/groovy.ts +45 -1
- package/src/extractors/haskell.ts +45 -4
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +191 -77
- package/src/extractors/kotlin.ts +4 -0
- package/src/extractors/objc.ts +171 -47
- package/src/extractors/python.ts +5 -3
- package/src/extractors/r.ts +104 -82
- package/src/extractors/scala.ts +24 -36
- package/src/extractors/solidity.ts +59 -78
- package/src/extractors/verilog.ts +83 -15
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +56 -56
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/mcp/tool-registry.ts +5 -0
- package/src/mcp/tools/semantic-search.ts +2 -0
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +16 -1
package/src/features/cochange.ts
CHANGED
|
@@ -137,77 +137,50 @@ export function computeCoChanges(
|
|
|
137
137
|
return { pairs: results, fileCommitCounts };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const db = openDb(dbPath);
|
|
153
|
-
initSchema(db);
|
|
154
|
-
|
|
155
|
-
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
156
|
-
|
|
157
|
-
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
158
|
-
closeDb(db);
|
|
159
|
-
return { error: `Not a git repository: ${repoRoot}` };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const since = opts.since || '1 year ago';
|
|
163
|
-
const minSupport = opts.minSupport ?? 3;
|
|
164
|
-
const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
|
|
165
|
-
|
|
166
|
-
// Check for incremental state
|
|
167
|
-
let afterSha: string | null = null;
|
|
168
|
-
if (!opts.full) {
|
|
169
|
-
try {
|
|
170
|
-
const row = db
|
|
171
|
-
.prepare<{ value: string }>(
|
|
172
|
-
"SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
|
|
173
|
-
)
|
|
174
|
-
.get();
|
|
175
|
-
if (row) afterSha = row.value;
|
|
176
|
-
} catch {
|
|
177
|
-
/* table may not exist yet */
|
|
178
|
-
}
|
|
140
|
+
/** Read the SHA of the most recently analyzed commit (incremental state). */
|
|
141
|
+
function loadLastAnalyzedSha(db: BetterSqlite3Database): string | null {
|
|
142
|
+
try {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare<{ value: string }>(
|
|
145
|
+
"SELECT value FROM co_change_meta WHERE key = 'last_analyzed_commit'",
|
|
146
|
+
)
|
|
147
|
+
.get();
|
|
148
|
+
return row ? row.value : null;
|
|
149
|
+
} catch {
|
|
150
|
+
/* table may not exist yet */
|
|
151
|
+
return null;
|
|
179
152
|
}
|
|
153
|
+
}
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
155
|
+
/** Wipe all co-change tables for a full re-scan. */
|
|
156
|
+
function clearCoChangeTables(db: BetterSqlite3Database): void {
|
|
157
|
+
db.exec('DELETE FROM co_changes');
|
|
158
|
+
db.exec('DELETE FROM co_change_meta');
|
|
159
|
+
db.exec('DELETE FROM file_commit_counts');
|
|
160
|
+
}
|
|
187
161
|
|
|
188
|
-
|
|
189
|
-
|
|
162
|
+
/** Collect the set of files currently tracked by the graph for filtering. */
|
|
163
|
+
function loadKnownFiles(db: BetterSqlite3Database): Set<string> | null {
|
|
190
164
|
try {
|
|
191
165
|
const rows = db.prepare<{ file: string }>('SELECT DISTINCT file FROM nodes').all();
|
|
192
|
-
|
|
166
|
+
return new Set(rows.map((r) => r.file));
|
|
193
167
|
} catch {
|
|
194
168
|
/* nodes table may not exist */
|
|
169
|
+
return null;
|
|
195
170
|
}
|
|
171
|
+
}
|
|
196
172
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Upsert per-file commit counts so Jaccard can be recomputed from totals
|
|
173
|
+
/** Upsert per-file commit counts and pair counts (Jaccard recomputed later). */
|
|
174
|
+
function persistCoChangeResults(
|
|
175
|
+
db: BetterSqlite3Database,
|
|
176
|
+
fileCommitCounts: Map<string, number>,
|
|
177
|
+
coChanges: Map<string, CoChangePair>,
|
|
178
|
+
): void {
|
|
205
179
|
const fileCountUpsert = db.prepare(`
|
|
206
180
|
INSERT INTO file_commit_counts (file, commit_count) VALUES (?, ?)
|
|
207
181
|
ON CONFLICT(file) DO UPDATE SET commit_count = commit_count + excluded.commit_count
|
|
208
182
|
`);
|
|
209
183
|
|
|
210
|
-
// Upsert pair counts (accumulate commit_count, jaccard placeholder — recomputed below)
|
|
211
184
|
const pairUpsert = db.prepare(`
|
|
212
185
|
INSERT INTO co_changes (file_a, file_b, commit_count, jaccard, last_commit_epoch)
|
|
213
186
|
VALUES (?, ?, ?, 0, ?)
|
|
@@ -226,24 +199,31 @@ export function analyzeCoChanges(
|
|
|
226
199
|
}
|
|
227
200
|
});
|
|
228
201
|
insertMany();
|
|
202
|
+
}
|
|
229
203
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (affectedFiles.length
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
FROM file_commit_counts fa, file_commit_counts fb
|
|
240
|
-
WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
|
|
204
|
+
/** Recompute Jaccard for every pair touching any file in `affectedFiles`. */
|
|
205
|
+
function recomputeJaccardForAffected(db: BetterSqlite3Database, affectedFiles: string[]): void {
|
|
206
|
+
if (affectedFiles.length === 0) return;
|
|
207
|
+
const ph = affectedFiles.map(() => '?').join(',');
|
|
208
|
+
db.prepare(`
|
|
209
|
+
UPDATE co_changes SET jaccard = (
|
|
210
|
+
SELECT CAST(co_changes.commit_count AS REAL) / (
|
|
211
|
+
COALESCE(fa.commit_count, 0) + COALESCE(fb.commit_count, 0) - co_changes.commit_count
|
|
241
212
|
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
213
|
+
FROM file_commit_counts fa, file_commit_counts fb
|
|
214
|
+
WHERE fa.file = co_changes.file_a AND fb.file = co_changes.file_b
|
|
215
|
+
)
|
|
216
|
+
WHERE file_a IN (${ph}) OR file_b IN (${ph})
|
|
217
|
+
`).run(...affectedFiles, ...affectedFiles);
|
|
218
|
+
}
|
|
245
219
|
|
|
246
|
-
|
|
220
|
+
/** Update co_change_meta with the latest analyzer run parameters. */
|
|
221
|
+
function updateCoChangeMeta(
|
|
222
|
+
db: BetterSqlite3Database,
|
|
223
|
+
commits: CommitEntry[],
|
|
224
|
+
since: string,
|
|
225
|
+
minSupport: number,
|
|
226
|
+
): void {
|
|
247
227
|
const metaUpsert = db.prepare(`
|
|
248
228
|
INSERT INTO co_change_meta (key, value) VALUES (?, ?)
|
|
249
229
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
@@ -254,6 +234,49 @@ export function analyzeCoChanges(
|
|
|
254
234
|
metaUpsert.run('analyzed_at', new Date().toISOString());
|
|
255
235
|
metaUpsert.run('since', since);
|
|
256
236
|
metaUpsert.run('min_support', String(minSupport));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function analyzeCoChanges(
|
|
240
|
+
customDbPath?: string,
|
|
241
|
+
opts: {
|
|
242
|
+
since?: string;
|
|
243
|
+
minSupport?: number;
|
|
244
|
+
maxFilesPerCommit?: number;
|
|
245
|
+
full?: boolean;
|
|
246
|
+
} = {},
|
|
247
|
+
):
|
|
248
|
+
| { pairsFound: number; commitsScanned: number; since: string; minSupport: number }
|
|
249
|
+
| { error: string } {
|
|
250
|
+
const dbPath = findDbPath(customDbPath);
|
|
251
|
+
const db = openDb(dbPath);
|
|
252
|
+
initSchema(db);
|
|
253
|
+
|
|
254
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
255
|
+
|
|
256
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
257
|
+
closeDb(db);
|
|
258
|
+
return { error: `Not a git repository: ${repoRoot}` };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const since = opts.since || '1 year ago';
|
|
262
|
+
const minSupport = opts.minSupport ?? 3;
|
|
263
|
+
const maxFilesPerCommit = opts.maxFilesPerCommit ?? 50;
|
|
264
|
+
|
|
265
|
+
const afterSha = opts.full ? null : loadLastAnalyzedSha(db);
|
|
266
|
+
if (opts.full) clearCoChangeTables(db);
|
|
267
|
+
|
|
268
|
+
const knownFiles = loadKnownFiles(db);
|
|
269
|
+
|
|
270
|
+
const { commits } = scanGitHistory(repoRoot, { since, afterSha });
|
|
271
|
+
const { pairs: coChanges, fileCommitCounts } = computeCoChanges(commits, {
|
|
272
|
+
minSupport,
|
|
273
|
+
maxFilesPerCommit,
|
|
274
|
+
knownFiles,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
persistCoChangeResults(db, fileCommitCounts, coChanges);
|
|
278
|
+
recomputeJaccardForAffected(db, [...fileCommitCounts.keys()]);
|
|
279
|
+
updateCoChangeMeta(db, commits, since, minSupport);
|
|
257
280
|
|
|
258
281
|
const totalPairs = db
|
|
259
282
|
.prepare<{ cnt: number }>('SELECT COUNT(*) as cnt FROM co_changes')
|
|
@@ -31,44 +31,36 @@ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
|
|
|
31
31
|
|
|
32
32
|
// ─── Halstead Metrics Computation ─────────────────────────────────────────
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Skip type annotation subtrees
|
|
48
|
-
if (rules?.skipTypes.has(node.type)) return;
|
|
34
|
+
/** Classify a tree-sitter node as a Halstead operator or operand,
|
|
35
|
+
* updating the running counts. Pure helper extracted from computeHalsteadMetrics
|
|
36
|
+
* to keep the dispatcher thin. */
|
|
37
|
+
function classifyHalsteadToken(
|
|
38
|
+
node: TreeSitterNode,
|
|
39
|
+
rules: HalsteadRules,
|
|
40
|
+
operators: Map<string, number>,
|
|
41
|
+
operands: Map<string, number>,
|
|
42
|
+
): void {
|
|
43
|
+
// Compound operators (non-leaf): count the node type as an operator
|
|
44
|
+
if (rules.compoundOperators.has(node.type)) {
|
|
45
|
+
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Leaf nodes: classify as operator or operand
|
|
49
|
+
if (node.childCount === 0) {
|
|
50
|
+
if (rules.operatorLeafTypes.has(node.type)) {
|
|
52
51
|
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (node.childCount === 0) {
|
|
57
|
-
if (rules?.operatorLeafTypes.has(node.type)) {
|
|
58
|
-
operators.set(node.type, (operators.get(node.type) || 0) + 1);
|
|
59
|
-
} else if (rules?.operandLeafTypes.has(node.type)) {
|
|
60
|
-
const text = node.text;
|
|
61
|
-
operands.set(text, (operands.get(text) || 0) + 1);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
66
|
-
walk(node.child(i));
|
|
52
|
+
} else if (rules.operandLeafTypes.has(node.type)) {
|
|
53
|
+
const text = node.text;
|
|
54
|
+
operands.set(text, (operands.get(text) || 0) + 1);
|
|
67
55
|
}
|
|
68
56
|
}
|
|
57
|
+
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
/** Build a HalsteadDerivedMetrics summary from the raw operator/operand counts. */
|
|
60
|
+
function summarizeHalsteadCounts(
|
|
61
|
+
operators: Map<string, number>,
|
|
62
|
+
operands: Map<string, number>,
|
|
63
|
+
): HalsteadDerivedMetrics {
|
|
72
64
|
const n1 = operators.size; // distinct operators
|
|
73
65
|
const n2 = operands.size; // distinct operands
|
|
74
66
|
let bigN1 = 0; // total operators
|
|
@@ -79,7 +71,6 @@ export function computeHalsteadMetrics(
|
|
|
79
71
|
const vocabulary = n1 + n2;
|
|
80
72
|
const length = bigN1 + bigN2;
|
|
81
73
|
|
|
82
|
-
// Guard against zero
|
|
83
74
|
const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
|
|
84
75
|
const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
|
|
85
76
|
const effort = difficulty * volume;
|
|
@@ -99,6 +90,31 @@ export function computeHalsteadMetrics(
|
|
|
99
90
|
};
|
|
100
91
|
}
|
|
101
92
|
|
|
93
|
+
export function computeHalsteadMetrics(
|
|
94
|
+
functionNode: TreeSitterNode,
|
|
95
|
+
language: string,
|
|
96
|
+
): HalsteadDerivedMetrics | null {
|
|
97
|
+
const rules = HALSTEAD_RULES.get(language) as HalsteadRules | undefined;
|
|
98
|
+
if (!rules) return null;
|
|
99
|
+
|
|
100
|
+
const operators = new Map<string, number>(); // type -> count
|
|
101
|
+
const operands = new Map<string, number>(); // text -> count
|
|
102
|
+
|
|
103
|
+
function walk(node: TreeSitterNode | null): void {
|
|
104
|
+
if (!node) return;
|
|
105
|
+
// Skip type annotation subtrees
|
|
106
|
+
if (rules?.skipTypes.has(node.type)) return;
|
|
107
|
+
classifyHalsteadToken(node, rules as HalsteadRules, operators, operands);
|
|
108
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
109
|
+
walk(node.child(i));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
walk(functionNode);
|
|
114
|
+
|
|
115
|
+
return summarizeHalsteadCounts(operators, operands);
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
// ─── LOC Metrics Computation ──────────────────────────────────────────────
|
|
103
119
|
// Delegated to ast-analysis/metrics.js; re-exported for backward compatibility.
|
|
104
120
|
export const computeLOCMetrics = _computeLOCMetrics;
|
|
@@ -535,6 +551,89 @@ function upsertAstComplexity(
|
|
|
535
551
|
return 1;
|
|
536
552
|
}
|
|
537
553
|
|
|
554
|
+
/** Decision outcome for a single definition during native bulk-row collection.
|
|
555
|
+
* - 'skip': the definition is legitimately ignorable (non-function, missing line,
|
|
556
|
+
* interface stub, unsupported language).
|
|
557
|
+
* - 'fallback': a genuine function body is missing precomputed complexity —
|
|
558
|
+
* the whole native fast path must abort to JS.
|
|
559
|
+
* - 'emit': the definition has complexity data and a row was (or will be) appended. */
|
|
560
|
+
type NativeRowDecision = 'skip' | 'fallback' | 'emit';
|
|
561
|
+
|
|
562
|
+
/** Classify a definition relative to the native bulk path. Returns
|
|
563
|
+
* 'skip' to ignore it, 'fallback' to bail out, or 'emit' if the row should be added. */
|
|
564
|
+
function classifyDefinitionForNativeBulk(
|
|
565
|
+
def: FileSymbols['definitions'][0],
|
|
566
|
+
langSupported: boolean,
|
|
567
|
+
): NativeRowDecision {
|
|
568
|
+
if (def.kind !== 'function' && def.kind !== 'method') return 'skip';
|
|
569
|
+
if (!def.line) return 'skip';
|
|
570
|
+
if (!def.complexity) {
|
|
571
|
+
// Interface/type property signatures and single-line stubs are extracted
|
|
572
|
+
// as methods but the native engine correctly never assigns complexity.
|
|
573
|
+
// Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
|
|
574
|
+
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
575
|
+
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) return 'skip';
|
|
576
|
+
// Languages without complexity rules will never have data — skip them
|
|
577
|
+
// rather than bailing out of the entire native bulk path.
|
|
578
|
+
if (!langSupported) return 'skip';
|
|
579
|
+
return 'fallback'; // genuine function body missing complexity — needs JS fallback
|
|
580
|
+
}
|
|
581
|
+
return 'emit';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Build a single native-bulk row from a definition with complexity data. */
|
|
585
|
+
function buildNativeBulkRow(
|
|
586
|
+
nodeId: number,
|
|
587
|
+
def: FileSymbols['definitions'][0],
|
|
588
|
+
): Record<string, unknown> {
|
|
589
|
+
const ch = def.complexity?.halstead;
|
|
590
|
+
const cl = def.complexity?.loc;
|
|
591
|
+
return {
|
|
592
|
+
nodeId,
|
|
593
|
+
cognitive: def.complexity?.cognitive ?? 0,
|
|
594
|
+
cyclomatic: def.complexity?.cyclomatic ?? 0,
|
|
595
|
+
maxNesting: def.complexity?.maxNesting ?? 0,
|
|
596
|
+
loc: cl ? cl.loc : 0,
|
|
597
|
+
sloc: cl ? cl.sloc : 0,
|
|
598
|
+
commentLines: cl ? cl.commentLines : 0,
|
|
599
|
+
halsteadN1: ch ? ch.n1 : 0,
|
|
600
|
+
halsteadN2: ch ? ch.n2 : 0,
|
|
601
|
+
halsteadBigN1: ch ? ch.bigN1 : 0,
|
|
602
|
+
halsteadBigN2: ch ? ch.bigN2 : 0,
|
|
603
|
+
halsteadVocabulary: ch ? ch.vocabulary : 0,
|
|
604
|
+
halsteadLength: ch ? ch.length : 0,
|
|
605
|
+
halsteadVolume: ch ? ch.volume : 0,
|
|
606
|
+
halsteadDifficulty: ch ? ch.difficulty : 0,
|
|
607
|
+
halsteadEffort: ch ? ch.effort : 0,
|
|
608
|
+
halsteadBugs: ch ? ch.bugs : 0,
|
|
609
|
+
maintainabilityIndex: def.complexity?.maintainabilityIndex ?? 0,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Try to collect a single file's definitions into native-bulk rows.
|
|
614
|
+
* Returns 'fallback' if any definition forces a JS fallback. */
|
|
615
|
+
function collectFileBulkRows(
|
|
616
|
+
db: BetterSqlite3Database,
|
|
617
|
+
relPath: string,
|
|
618
|
+
symbols: FileSymbols,
|
|
619
|
+
rows: Array<Record<string, unknown>>,
|
|
620
|
+
): NativeRowDecision {
|
|
621
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
622
|
+
const langId = symbols._langId || '';
|
|
623
|
+
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
624
|
+
|
|
625
|
+
for (const def of symbols.definitions) {
|
|
626
|
+
const decision = classifyDefinitionForNativeBulk(def, langSupported);
|
|
627
|
+
if (decision === 'skip') continue;
|
|
628
|
+
if (decision === 'fallback') return 'fallback';
|
|
629
|
+
|
|
630
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
631
|
+
if (!nodeId) continue;
|
|
632
|
+
rows.push(buildNativeBulkRow(nodeId, def));
|
|
633
|
+
}
|
|
634
|
+
return 'emit';
|
|
635
|
+
}
|
|
636
|
+
|
|
538
637
|
/** Collect native bulk-insert rows from precomputed complexity data.
|
|
539
638
|
* Returns the rows array, or null if any definition is missing complexity
|
|
540
639
|
* (signalling that JS fallback is needed). */
|
|
@@ -543,53 +642,9 @@ function collectNativeBulkRows(
|
|
|
543
642
|
fileSymbols: Map<string, FileSymbols>,
|
|
544
643
|
): Array<Record<string, unknown>> | null {
|
|
545
644
|
const rows: Array<Record<string, unknown>> = [];
|
|
546
|
-
|
|
547
645
|
for (const [relPath, symbols] of fileSymbols) {
|
|
548
|
-
|
|
549
|
-
const langId = symbols._langId || '';
|
|
550
|
-
const langSupported = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
551
|
-
|
|
552
|
-
for (const def of symbols.definitions) {
|
|
553
|
-
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
554
|
-
if (!def.line) continue;
|
|
555
|
-
// Interface/type property signatures and single-line stubs are extracted
|
|
556
|
-
// as methods but the native engine correctly never assigns complexity.
|
|
557
|
-
// Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out
|
|
558
|
-
// of the native bulk-insert path for every TypeScript codebase (#846).
|
|
559
|
-
if (!def.complexity) {
|
|
560
|
-
if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) continue;
|
|
561
|
-
// Languages without complexity rules will never have data — skip them
|
|
562
|
-
// rather than bailing out of the entire native bulk path.
|
|
563
|
-
if (!langSupported) continue;
|
|
564
|
-
return null; // genuine function body missing complexity — needs JS fallback
|
|
565
|
-
}
|
|
566
|
-
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
567
|
-
if (!nodeId) continue;
|
|
568
|
-
const ch = def.complexity.halstead;
|
|
569
|
-
const cl = def.complexity.loc;
|
|
570
|
-
rows.push({
|
|
571
|
-
nodeId,
|
|
572
|
-
cognitive: def.complexity.cognitive ?? 0,
|
|
573
|
-
cyclomatic: def.complexity.cyclomatic ?? 0,
|
|
574
|
-
maxNesting: def.complexity.maxNesting ?? 0,
|
|
575
|
-
loc: cl ? cl.loc : 0,
|
|
576
|
-
sloc: cl ? cl.sloc : 0,
|
|
577
|
-
commentLines: cl ? cl.commentLines : 0,
|
|
578
|
-
halsteadN1: ch ? ch.n1 : 0,
|
|
579
|
-
halsteadN2: ch ? ch.n2 : 0,
|
|
580
|
-
halsteadBigN1: ch ? ch.bigN1 : 0,
|
|
581
|
-
halsteadBigN2: ch ? ch.bigN2 : 0,
|
|
582
|
-
halsteadVocabulary: ch ? ch.vocabulary : 0,
|
|
583
|
-
halsteadLength: ch ? ch.length : 0,
|
|
584
|
-
halsteadVolume: ch ? ch.volume : 0,
|
|
585
|
-
halsteadDifficulty: ch ? ch.difficulty : 0,
|
|
586
|
-
halsteadEffort: ch ? ch.effort : 0,
|
|
587
|
-
halsteadBugs: ch ? ch.bugs : 0,
|
|
588
|
-
maintainabilityIndex: def.complexity.maintainabilityIndex ?? 0,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
646
|
+
if (collectFileBulkRows(db, relPath, symbols, rows) === 'fallback') return null;
|
|
591
647
|
}
|
|
592
|
-
|
|
593
648
|
return rows;
|
|
594
649
|
}
|
|
595
650
|
|
package/src/features/dataflow.ts
CHANGED
|
@@ -675,6 +675,51 @@ interface BfsParentEntry {
|
|
|
675
675
|
expression: string;
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
type DataflowNeighbor = {
|
|
679
|
+
id: number;
|
|
680
|
+
file: string;
|
|
681
|
+
edge_kind: string;
|
|
682
|
+
expression: string;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
interface DataflowBfsState {
|
|
686
|
+
visited: Set<number>;
|
|
687
|
+
parent: Map<number, BfsParentEntry>;
|
|
688
|
+
nextQueue: number[];
|
|
689
|
+
found: boolean;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Process a single neighbor in the dataflow BFS. Returns true once the target
|
|
694
|
+
* has been reached so the caller can stop expanding.
|
|
695
|
+
*/
|
|
696
|
+
function processDataflowNeighbor(
|
|
697
|
+
n: DataflowNeighbor,
|
|
698
|
+
currentId: number,
|
|
699
|
+
targetId: number,
|
|
700
|
+
noTests: boolean,
|
|
701
|
+
state: DataflowBfsState,
|
|
702
|
+
): boolean {
|
|
703
|
+
if (noTests && isTestFile(n.file)) return false;
|
|
704
|
+
const entry: BfsParentEntry = {
|
|
705
|
+
parentId: currentId,
|
|
706
|
+
edgeKind: n.edge_kind,
|
|
707
|
+
expression: n.expression,
|
|
708
|
+
};
|
|
709
|
+
if (n.id === targetId) {
|
|
710
|
+
if (!state.found) {
|
|
711
|
+
state.found = true;
|
|
712
|
+
state.parent.set(n.id, entry);
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
if (state.visited.has(n.id)) return false;
|
|
717
|
+
state.visited.add(n.id);
|
|
718
|
+
state.parent.set(n.id, entry);
|
|
719
|
+
state.nextQueue.push(n.id);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
678
723
|
/** BFS through dataflow edges to find a path from source to target. */
|
|
679
724
|
function bfsDataflowPath(
|
|
680
725
|
db: BetterSqlite3Database,
|
|
@@ -689,50 +734,28 @@ function bfsDataflowPath(
|
|
|
689
734
|
WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
|
|
690
735
|
);
|
|
691
736
|
|
|
692
|
-
const
|
|
693
|
-
|
|
737
|
+
const state: DataflowBfsState = {
|
|
738
|
+
visited: new Set<number>([sourceId]),
|
|
739
|
+
parent: new Map<number, BfsParentEntry>(),
|
|
740
|
+
nextQueue: [],
|
|
741
|
+
found: false,
|
|
742
|
+
};
|
|
694
743
|
let queue = [sourceId];
|
|
695
|
-
let found = false;
|
|
696
744
|
|
|
697
745
|
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
698
|
-
|
|
746
|
+
state.nextQueue = [];
|
|
699
747
|
for (const currentId of queue) {
|
|
700
|
-
const neighbors = neighborStmt.all(currentId) as
|
|
701
|
-
id: number;
|
|
702
|
-
file: string;
|
|
703
|
-
edge_kind: string;
|
|
704
|
-
expression: string;
|
|
705
|
-
}>;
|
|
748
|
+
const neighbors = neighborStmt.all(currentId) as DataflowNeighbor[];
|
|
706
749
|
for (const n of neighbors) {
|
|
707
|
-
|
|
708
|
-
if (n.id === targetId) {
|
|
709
|
-
if (!found) {
|
|
710
|
-
found = true;
|
|
711
|
-
parent.set(n.id, {
|
|
712
|
-
parentId: currentId,
|
|
713
|
-
edgeKind: n.edge_kind,
|
|
714
|
-
expression: n.expression,
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (!visited.has(n.id)) {
|
|
720
|
-
visited.add(n.id);
|
|
721
|
-
parent.set(n.id, {
|
|
722
|
-
parentId: currentId,
|
|
723
|
-
edgeKind: n.edge_kind,
|
|
724
|
-
expression: n.expression,
|
|
725
|
-
});
|
|
726
|
-
nextQueue.push(n.id);
|
|
727
|
-
}
|
|
750
|
+
processDataflowNeighbor(n, currentId, targetId, noTests, state);
|
|
728
751
|
}
|
|
729
752
|
}
|
|
730
|
-
if (found) break;
|
|
731
|
-
queue = nextQueue;
|
|
753
|
+
if (state.found) break;
|
|
754
|
+
queue = state.nextQueue;
|
|
732
755
|
if (queue.length === 0) break;
|
|
733
756
|
}
|
|
734
757
|
|
|
735
|
-
return found ? parent : null;
|
|
758
|
+
return state.found ? state.parent : null;
|
|
736
759
|
}
|
|
737
760
|
|
|
738
761
|
/** Reconstruct a path from BFS parent map. */
|
package/src/features/flow.ts
CHANGED
|
@@ -133,6 +133,41 @@ interface BfsState {
|
|
|
133
133
|
truncated: boolean;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
interface FlowBfsFrame {
|
|
137
|
+
visited: Set<number>;
|
|
138
|
+
cycles: Array<{ from: string; to: string; depth: number }>;
|
|
139
|
+
nodeDepths: Map<number, number>;
|
|
140
|
+
idToNode: Map<number, NodeInfo>;
|
|
141
|
+
nextFrontier: number[];
|
|
142
|
+
levelNodes: NodeInfo[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Process one callee row, recording cycle hits or expanding frontier. */
|
|
146
|
+
function processFlowCallee(
|
|
147
|
+
c: CalleeRow,
|
|
148
|
+
fid: number,
|
|
149
|
+
depth: number,
|
|
150
|
+
noTests: boolean,
|
|
151
|
+
frame: FlowBfsFrame,
|
|
152
|
+
): void {
|
|
153
|
+
if (noTests && isTestFile(c.file)) return;
|
|
154
|
+
|
|
155
|
+
if (frame.visited.has(c.id)) {
|
|
156
|
+
const fromNode = frame.idToNode.get(fid);
|
|
157
|
+
if (fromNode) {
|
|
158
|
+
frame.cycles.push({ from: fromNode.name, to: c.name, depth });
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
frame.visited.add(c.id);
|
|
164
|
+
frame.nextFrontier.push(c.id);
|
|
165
|
+
const nodeInfo: NodeInfo = toSymbolRef(c);
|
|
166
|
+
frame.levelNodes.push(nodeInfo);
|
|
167
|
+
frame.nodeDepths.set(c.id, depth);
|
|
168
|
+
frame.idToNode.set(c.id, nodeInfo);
|
|
169
|
+
}
|
|
170
|
+
|
|
136
171
|
/** Forward BFS through callees, collecting steps, cycles, and node depth info. */
|
|
137
172
|
function bfsCallees(
|
|
138
173
|
db: ReturnType<typeof openReadonlyOrFail>,
|
|
@@ -157,37 +192,26 @@ function bfsCallees(
|
|
|
157
192
|
);
|
|
158
193
|
|
|
159
194
|
for (let d = 1; d <= maxDepth; d++) {
|
|
160
|
-
const
|
|
161
|
-
|
|
195
|
+
const frame: FlowBfsFrame = {
|
|
196
|
+
visited,
|
|
197
|
+
cycles,
|
|
198
|
+
nodeDepths,
|
|
199
|
+
idToNode,
|
|
200
|
+
nextFrontier: [],
|
|
201
|
+
levelNodes: [],
|
|
202
|
+
};
|
|
162
203
|
|
|
163
204
|
for (const fid of frontier) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
for (const c of callees) {
|
|
167
|
-
if (noTests && isTestFile(c.file)) continue;
|
|
168
|
-
|
|
169
|
-
if (visited.has(c.id)) {
|
|
170
|
-
const fromNode = idToNode.get(fid);
|
|
171
|
-
if (fromNode) {
|
|
172
|
-
cycles.push({ from: fromNode.name, to: c.name, depth: d });
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
visited.add(c.id);
|
|
178
|
-
nextFrontier.push(c.id);
|
|
179
|
-
const nodeInfo: NodeInfo = toSymbolRef(c);
|
|
180
|
-
levelNodes.push(nodeInfo);
|
|
181
|
-
nodeDepths.set(c.id, d);
|
|
182
|
-
idToNode.set(c.id, nodeInfo);
|
|
205
|
+
for (const c of calleesStmt.all(fid)) {
|
|
206
|
+
processFlowCallee(c, fid, d, noTests, frame);
|
|
183
207
|
}
|
|
184
208
|
}
|
|
185
209
|
|
|
186
|
-
if (levelNodes.length > 0) {
|
|
187
|
-
steps.push({ depth: d, nodes: levelNodes });
|
|
210
|
+
if (frame.levelNodes.length > 0) {
|
|
211
|
+
steps.push({ depth: d, nodes: frame.levelNodes });
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
frontier = nextFrontier;
|
|
214
|
+
frontier = frame.nextFrontier;
|
|
191
215
|
if (frontier.length === 0) break;
|
|
192
216
|
if (d === maxDepth && frontier.length > 0) truncated = true;
|
|
193
217
|
}
|