@optave/codegraph 3.8.0 → 3.9.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 +13 -8
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +137 -86
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/metrics.d.ts +0 -3
- package/dist/ast-analysis/metrics.d.ts.map +1 -1
- package/dist/ast-analysis/metrics.js +30 -13
- package/dist/ast-analysis/metrics.js.map +1 -1
- package/dist/ast-analysis/shared.d.ts.map +1 -1
- package/dist/ast-analysis/shared.js +24 -19
- package/dist/ast-analysis/shared.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +55 -39
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitor.js +91 -70
- package/dist/ast-analysis/visitor.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 +54 -58
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js +81 -39
- package/dist/ast-analysis/visitors/complexity-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 +57 -38
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/branch-compare.d.ts.map +1 -1
- package/dist/cli/commands/branch-compare.js +4 -0
- package/dist/cli/commands/branch-compare.js.map +1 -1
- package/dist/cli/commands/diff-impact.d.ts.map +1 -1
- package/dist/cli/commands/diff-impact.js +2 -1
- package/dist/cli/commands/diff-impact.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +3 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +16 -2
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +29 -26
- package/dist/db/connection.js.map +1 -1
- package/dist/db/query-builder.d.ts.map +1 -1
- package/dist/db/query-builder.js +16 -5
- package/dist/db/query-builder.js.map +1 -1
- package/dist/db/repository/base.d.ts +16 -0
- package/dist/db/repository/base.d.ts.map +1 -1
- package/dist/db/repository/base.js +31 -0
- package/dist/db/repository/base.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts +7 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +100 -1
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/db/repository/nodes.d.ts.map +1 -1
- package/dist/db/repository/nodes.js +8 -4
- package/dist/db/repository/nodes.js.map +1 -1
- package/dist/db/repository/sqlite-repository.d.ts +4 -0
- package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
- package/dist/db/repository/sqlite-repository.js +51 -0
- package/dist/db/repository/sqlite-repository.js.map +1 -1
- package/dist/domain/analysis/brief.d.ts.map +1 -1
- package/dist/domain/analysis/brief.js +13 -17
- package/dist/domain/analysis/brief.js.map +1 -1
- package/dist/domain/analysis/context.d.ts.map +1 -1
- package/dist/domain/analysis/context.js +14 -11
- package/dist/domain/analysis/context.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +64 -59
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts +2 -7
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +33 -31
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/implementations.d.ts.map +1 -1
- package/dist/domain/analysis/implementations.js +11 -19
- package/dist/domain/analysis/implementations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +55 -76
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/analysis/query-helpers.d.ts +7 -0
- package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
- package/dist/domain/analysis/query-helpers.js +15 -1
- package/dist/domain/analysis/query-helpers.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +352 -107
- 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 +49 -18
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +2 -2
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/cycles.d.ts +6 -0
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +114 -22
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/resolve.js +1 -1
- package/dist/domain/graph/resolve.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts +2 -0
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +170 -75
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +3 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +141 -89
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.js +1 -1
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +4 -3
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +23 -8
- 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 +29 -18
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/extractors/go.js +36 -33
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -29
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.js +58 -46
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.js +65 -54
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +84 -78
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/python.js +29 -24
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/rust.js +41 -32
- package/dist/extractors/rust.js.map +1 -1
- package/dist/extractors/solidity.js +58 -67
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +83 -81
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +58 -60
- package/dist/extractors/zig.js.map +1 -1
- package/dist/features/ast.d.ts +16 -14
- package/dist/features/ast.d.ts.map +1 -1
- package/dist/features/ast.js +83 -81
- package/dist/features/ast.js.map +1 -1
- package/dist/features/audit.d.ts.map +1 -1
- package/dist/features/audit.js +8 -6
- package/dist/features/audit.js.map +1 -1
- package/dist/features/branch-compare.d.ts.map +1 -1
- package/dist/features/branch-compare.js +69 -72
- package/dist/features/branch-compare.js.map +1 -1
- package/dist/features/communities.d.ts.map +1 -1
- package/dist/features/communities.js +19 -7
- package/dist/features/communities.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +120 -125
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +136 -137
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +84 -79
- package/dist/features/flow.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +69 -65
- package/dist/features/structure-query.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +70 -55
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/partition.js +288 -266
- package/dist/graph/algorithms/leiden/partition.js.map +1 -1
- package/dist/graph/model.d.ts.map +1 -1
- package/dist/graph/model.js +5 -1
- package/dist/graph/model.js.map +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +6 -4
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/suppress.d.ts +25 -0
- package/dist/infrastructure/suppress.d.ts.map +1 -0
- package/dist/infrastructure/suppress.js +43 -0
- package/dist/infrastructure/suppress.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -24
- package/dist/mcp/server.js.map +1 -1
- package/dist/presentation/dataflow.d.ts.map +1 -1
- package/dist/presentation/dataflow.js +47 -38
- package/dist/presentation/dataflow.js.map +1 -1
- package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
- package/dist/presentation/diff-impact-mermaid.js +60 -51
- package/dist/presentation/diff-impact-mermaid.js.map +1 -1
- package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
- package/dist/presentation/queries-cli/exports.js +20 -14
- package/dist/presentation/queries-cli/exports.js.map +1 -1
- package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
- package/dist/presentation/queries-cli/impact.js +15 -13
- package/dist/presentation/queries-cli/impact.js.map +1 -1
- package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
- package/dist/presentation/queries-cli/inspect.js +101 -79
- package/dist/presentation/queries-cli/inspect.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +25 -16
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/presentation/queries-cli/path.js +26 -20
- package/dist/presentation/queries-cli/path.js.map +1 -1
- package/dist/presentation/result-formatter.d.ts +10 -0
- package/dist/presentation/result-formatter.d.ts.map +1 -1
- package/dist/presentation/result-formatter.js +16 -1
- package/dist/presentation/result-formatter.js.map +1 -1
- package/dist/presentation/viewer.d.ts.map +1 -1
- package/dist/presentation/viewer.js +18 -12
- package/dist/presentation/viewer.js.map +1 -1
- package/dist/shared/errors.d.ts +5 -0
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/errors.js +5 -0
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/hierarchy.d.ts +8 -2
- package/dist/shared/hierarchy.d.ts.map +1 -1
- package/dist/shared/hierarchy.js +42 -1
- package/dist/shared/hierarchy.js.map +1 -1
- package/dist/shared/normalize.d.ts +6 -1
- package/dist/shared/normalize.d.ts.map +1 -1
- package/dist/shared/normalize.js +20 -12
- package/dist/shared/normalize.js.map +1 -1
- package/dist/shared/paginate.d.ts +0 -9
- package/dist/shared/paginate.d.ts.map +1 -1
- package/dist/shared/paginate.js +0 -15
- package/dist/shared/paginate.js.map +1 -1
- package/dist/types.d.ts +12 -5
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +176 -104
- package/src/ast-analysis/metrics.ts +33 -11
- package/src/ast-analysis/shared.ts +33 -24
- package/src/ast-analysis/visitor-utils.ts +52 -32
- package/src/ast-analysis/visitor.ts +132 -71
- package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
- package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
- package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
- package/src/cli/commands/branch-compare.ts +4 -0
- package/src/cli/commands/diff-impact.ts +2 -1
- package/src/cli/commands/info.ts +3 -2
- package/src/cli/commands/watch.ts +16 -2
- package/src/db/connection.ts +29 -28
- package/src/db/query-builder.ts +15 -3
- package/src/db/repository/base.ts +34 -0
- package/src/db/repository/native-repository.ts +104 -1
- package/src/db/repository/nodes.ts +13 -8
- package/src/db/repository/sqlite-repository.ts +55 -0
- package/src/domain/analysis/brief.ts +15 -25
- package/src/domain/analysis/context.ts +17 -10
- package/src/domain/analysis/dependencies.ts +77 -81
- package/src/domain/analysis/fn-impact.ts +36 -43
- package/src/domain/analysis/implementations.ts +11 -17
- package/src/domain/analysis/module-map.ts +58 -92
- package/src/domain/analysis/query-helpers.ts +18 -1
- package/src/domain/graph/builder/pipeline.ts +409 -99
- package/src/domain/graph/builder/stages/build-edges.ts +45 -19
- package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
- package/src/domain/graph/builder/stages/finalize.ts +2 -2
- package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
- package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
- package/src/domain/graph/cycles.ts +110 -23
- package/src/domain/graph/resolve.ts +1 -1
- package/src/domain/graph/watcher.ts +202 -96
- package/src/domain/parser.ts +143 -89
- package/src/domain/search/generator.ts +1 -1
- package/src/domain/search/models.ts +26 -7
- package/src/domain/search/search/hybrid.ts +69 -51
- package/src/extractors/go.ts +43 -33
- package/src/extractors/helpers.ts +37 -23
- package/src/extractors/java.ts +66 -47
- package/src/extractors/javascript.ts +66 -54
- package/src/extractors/kotlin.ts +84 -77
- package/src/extractors/python.ts +31 -25
- package/src/extractors/rust.ts +37 -29
- package/src/extractors/solidity.ts +57 -61
- package/src/extractors/swift.ts +81 -80
- package/src/extractors/zig.ts +58 -61
- package/src/features/ast.ts +130 -110
- package/src/features/audit.ts +8 -6
- package/src/features/branch-compare.ts +105 -79
- package/src/features/communities.ts +25 -10
- package/src/features/complexity.ts +171 -134
- package/src/features/dataflow.ts +165 -175
- package/src/features/flow.ts +129 -92
- package/src/features/structure-query.ts +79 -64
- package/src/graph/algorithms/leiden/optimiser.ts +99 -55
- package/src/graph/algorithms/leiden/partition.ts +359 -294
- package/src/graph/model.ts +6 -1
- package/src/infrastructure/config.ts +6 -4
- package/src/infrastructure/suppress.ts +47 -0
- package/src/mcp/server.ts +53 -37
- package/src/presentation/dataflow.ts +50 -44
- package/src/presentation/diff-impact-mermaid.ts +104 -62
- package/src/presentation/queries-cli/exports.ts +21 -13
- package/src/presentation/queries-cli/impact.ts +15 -13
- package/src/presentation/queries-cli/inspect.ts +100 -81
- package/src/presentation/queries-cli/overview.ts +26 -16
- package/src/presentation/queries-cli/path.ts +33 -25
- package/src/presentation/result-formatter.ts +19 -1
- package/src/presentation/viewer.ts +42 -14
- package/src/shared/errors.ts +6 -0
- package/src/shared/hierarchy.ts +50 -2
- package/src/shared/normalize.ts +31 -12
- package/src/shared/paginate.ts +0 -17
- package/src/types.ts +26 -5
|
@@ -8,6 +8,7 @@ import { kindIcon } from '../domain/queries.js';
|
|
|
8
8
|
import { debug } from '../infrastructure/logger.js';
|
|
9
9
|
import { getNative, isNativeAvailable } from '../infrastructure/native.js';
|
|
10
10
|
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
11
|
+
import { toErrorMessage } from '../shared/errors.js';
|
|
11
12
|
import type { EngineMode, NativeDatabase } from '../types.js';
|
|
12
13
|
|
|
13
14
|
// ─── Git Helpers ────────────────────────────────────────────────────────
|
|
@@ -20,7 +21,8 @@ function validateGitRef(repoRoot: string, ref: string): string | null {
|
|
|
20
21
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
21
22
|
}).trim();
|
|
22
23
|
return sha;
|
|
23
|
-
} catch {
|
|
24
|
+
} catch (e) {
|
|
25
|
+
debug(`validateGitRef failed for "${ref}": ${toErrorMessage(e)}`);
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -50,11 +52,12 @@ function removeWorktree(repoRoot: string, dir: string): void {
|
|
|
50
52
|
encoding: 'utf-8',
|
|
51
53
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
54
|
});
|
|
53
|
-
} catch {
|
|
55
|
+
} catch (e) {
|
|
56
|
+
debug(`removeWorktree: git worktree remove failed for ${dir}: ${toErrorMessage(e)}`);
|
|
54
57
|
try {
|
|
55
58
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
59
|
+
} catch (rmErr) {
|
|
60
|
+
debug(`removeWorktree: rmSync fallback failed for ${dir}: ${toErrorMessage(rmErr)}`);
|
|
58
61
|
}
|
|
59
62
|
try {
|
|
60
63
|
execFileSync('git', ['worktree', 'prune'], {
|
|
@@ -62,8 +65,8 @@ function removeWorktree(repoRoot: string, dir: string): void {
|
|
|
62
65
|
encoding: 'utf-8',
|
|
63
66
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
67
|
});
|
|
65
|
-
} catch {
|
|
66
|
-
|
|
68
|
+
} catch (pruneErr) {
|
|
69
|
+
debug(`removeWorktree: git worktree prune failed: ${toErrorMessage(pruneErr)}`);
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -117,7 +120,7 @@ function loadSymbolsFromDb(
|
|
|
117
120
|
const native = getNative();
|
|
118
121
|
nativeDb = native.NativeDatabase.openReadonly(dbPath);
|
|
119
122
|
} catch (e) {
|
|
120
|
-
debug(`loadSymbolsFromDb: native path failed: ${(e
|
|
123
|
+
debug(`loadSymbolsFromDb: native path failed: ${toErrorMessage(e)}`);
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -205,8 +208,8 @@ function loadSymbolsFromDb(
|
|
|
205
208
|
if (nativeDb) {
|
|
206
209
|
try {
|
|
207
210
|
nativeDb.close();
|
|
208
|
-
} catch {
|
|
209
|
-
|
|
211
|
+
} catch (e) {
|
|
212
|
+
debug(`loadSymbolsFromDb: nativeDb close failed: ${toErrorMessage(e)}`);
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
}
|
|
@@ -360,6 +363,38 @@ interface BranchCompareResult {
|
|
|
360
363
|
summary?: BranchCompareSummary;
|
|
361
364
|
}
|
|
362
365
|
|
|
366
|
+
function attachImpactToSymbols(
|
|
367
|
+
symbols: SymbolInfo[],
|
|
368
|
+
dbPath: string,
|
|
369
|
+
_baseSymbols: Map<string, SymbolInfo>,
|
|
370
|
+
maxDepth: number,
|
|
371
|
+
noTests: boolean,
|
|
372
|
+
): void {
|
|
373
|
+
for (const sym of symbols) {
|
|
374
|
+
const symCallers = loadCallersFromDb(dbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
|
|
375
|
+
(sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function attachImpactToChanged(
|
|
380
|
+
changed: ChangedSymbol[],
|
|
381
|
+
dbPath: string,
|
|
382
|
+
baseSymbols: Map<string, SymbolInfo>,
|
|
383
|
+
maxDepth: number,
|
|
384
|
+
noTests: boolean,
|
|
385
|
+
): void {
|
|
386
|
+
for (const sym of changed) {
|
|
387
|
+
const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
|
|
388
|
+
const symCallers = loadCallersFromDb(
|
|
389
|
+
dbPath,
|
|
390
|
+
baseSym?.id ? [baseSym.id] : [],
|
|
391
|
+
maxDepth,
|
|
392
|
+
noTests,
|
|
393
|
+
);
|
|
394
|
+
sym.impact = symCallers;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
363
398
|
export async function branchCompareData(
|
|
364
399
|
baseRef: string,
|
|
365
400
|
targetRef: string,
|
|
@@ -376,7 +411,8 @@ export async function branchCompareData(
|
|
|
376
411
|
encoding: 'utf-8',
|
|
377
412
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
378
413
|
});
|
|
379
|
-
} catch {
|
|
414
|
+
} catch (e) {
|
|
415
|
+
debug(`branchCompareData: git check failed: ${toErrorMessage(e)}`);
|
|
380
416
|
return { error: 'Not a git repository' };
|
|
381
417
|
}
|
|
382
418
|
|
|
@@ -440,20 +476,8 @@ export async function branchCompareData(
|
|
|
440
476
|
const removedImpact = loadCallersFromDb(baseDbPath, removedIds, maxDepth, noTests);
|
|
441
477
|
const changedImpact = loadCallersFromDb(baseDbPath, changedIds, maxDepth, noTests);
|
|
442
478
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
(sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
|
|
446
|
-
}
|
|
447
|
-
for (const sym of changed) {
|
|
448
|
-
const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
|
|
449
|
-
const symCallers = loadCallersFromDb(
|
|
450
|
-
baseDbPath,
|
|
451
|
-
baseSym?.id ? [baseSym.id] : [],
|
|
452
|
-
maxDepth,
|
|
453
|
-
noTests,
|
|
454
|
-
);
|
|
455
|
-
sym.impact = symCallers;
|
|
456
|
-
}
|
|
479
|
+
attachImpactToSymbols(removed, baseDbPath, baseSymbols, maxDepth, noTests);
|
|
480
|
+
attachImpactToChanged(changed, baseDbPath, baseSymbols, maxDepth, noTests);
|
|
457
481
|
|
|
458
482
|
const allImpacted = new Set<string>();
|
|
459
483
|
for (const c of removedImpact) allImpacted.add(`${c.file}:${c.name}`);
|
|
@@ -489,20 +513,66 @@ export async function branchCompareData(
|
|
|
489
513
|
},
|
|
490
514
|
};
|
|
491
515
|
} catch (err) {
|
|
492
|
-
return { error: (err
|
|
516
|
+
return { error: toErrorMessage(err) };
|
|
493
517
|
} finally {
|
|
494
518
|
removeWorktree(repoRoot, baseDir);
|
|
495
519
|
removeWorktree(repoRoot, targetDir);
|
|
496
520
|
try {
|
|
497
521
|
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
498
|
-
} catch {
|
|
499
|
-
|
|
522
|
+
} catch (cleanupErr) {
|
|
523
|
+
debug(`branchCompareData: temp cleanup failed: ${toErrorMessage(cleanupErr)}`);
|
|
500
524
|
}
|
|
501
525
|
}
|
|
502
526
|
}
|
|
503
527
|
|
|
504
528
|
// ─── Mermaid Output ─────────────────────────────────────────────────────
|
|
505
529
|
|
|
530
|
+
interface MermaidNodeIdState {
|
|
531
|
+
counter: number;
|
|
532
|
+
map: Map<string, string>;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function mermaidNodeId(state: MermaidNodeIdState, key: string): string {
|
|
536
|
+
if (!state.map.has(key)) {
|
|
537
|
+
state.map.set(key, `n${state.counter++}`);
|
|
538
|
+
}
|
|
539
|
+
return state.map.get(key)!;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function addMermaidSubgraph(
|
|
543
|
+
lines: string[],
|
|
544
|
+
state: MermaidNodeIdState,
|
|
545
|
+
prefix: string,
|
|
546
|
+
label: string,
|
|
547
|
+
symbols: Array<{ kind: string; file: string; name: string }>,
|
|
548
|
+
fillColor: string,
|
|
549
|
+
strokeColor: string,
|
|
550
|
+
): void {
|
|
551
|
+
if (symbols.length === 0) return;
|
|
552
|
+
lines.push(` subgraph sg_${prefix}["${label}"]`);
|
|
553
|
+
for (const sym of symbols) {
|
|
554
|
+
const key = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
|
|
555
|
+
const nid = mermaidNodeId(state, key);
|
|
556
|
+
lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
|
|
557
|
+
}
|
|
558
|
+
lines.push(' end');
|
|
559
|
+
lines.push(` style sg_${prefix} fill:${fillColor},stroke:${strokeColor}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function collectImpactedCallers(
|
|
563
|
+
impactSources: Array<{ impact?: CallerInfo[] }>,
|
|
564
|
+
): Map<string, CallerInfo> {
|
|
565
|
+
const allImpacted = new Map<string, CallerInfo>();
|
|
566
|
+
for (const sym of impactSources) {
|
|
567
|
+
if (!sym.impact) continue;
|
|
568
|
+
for (const c of sym.impact) {
|
|
569
|
+
const key = `impact::${c.kind}::${c.file}::${c.name}`;
|
|
570
|
+
if (!allImpacted.has(key)) allImpacted.set(key, c);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return allImpacted;
|
|
574
|
+
}
|
|
575
|
+
|
|
506
576
|
export function branchCompareMermaid(data: BranchCompareResult): string {
|
|
507
577
|
if (data.error) return data.error;
|
|
508
578
|
if (
|
|
@@ -514,63 +584,19 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
|
|
|
514
584
|
}
|
|
515
585
|
|
|
516
586
|
const lines = ['flowchart TB'];
|
|
517
|
-
|
|
518
|
-
const nodeIdMap = new Map<string, string>();
|
|
519
|
-
|
|
520
|
-
function nodeId(key: string): string {
|
|
521
|
-
if (!nodeIdMap.has(key)) {
|
|
522
|
-
nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
523
|
-
}
|
|
524
|
-
return nodeIdMap.get(key)!;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (data.added && data.added.length > 0) {
|
|
528
|
-
lines.push(' subgraph sg_added["Added"]');
|
|
529
|
-
for (const sym of data.added) {
|
|
530
|
-
const key = `added::${sym.kind}::${sym.file}::${sym.name}`;
|
|
531
|
-
const nid = nodeId(key);
|
|
532
|
-
lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
|
|
533
|
-
}
|
|
534
|
-
lines.push(' end');
|
|
535
|
-
lines.push(' style sg_added fill:#e8f5e9,stroke:#4caf50');
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (data.removed && data.removed.length > 0) {
|
|
539
|
-
lines.push(' subgraph sg_removed["Removed"]');
|
|
540
|
-
for (const sym of data.removed) {
|
|
541
|
-
const key = `removed::${sym.kind}::${sym.file}::${sym.name}`;
|
|
542
|
-
const nid = nodeId(key);
|
|
543
|
-
lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
|
|
544
|
-
}
|
|
545
|
-
lines.push(' end');
|
|
546
|
-
lines.push(' style sg_removed fill:#ffebee,stroke:#f44336');
|
|
547
|
-
}
|
|
587
|
+
const state: MermaidNodeIdState = { counter: 0, map: new Map() };
|
|
548
588
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const key = `changed::${sym.kind}::${sym.file}::${sym.name}`;
|
|
553
|
-
const nid = nodeId(key);
|
|
554
|
-
lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
|
|
555
|
-
}
|
|
556
|
-
lines.push(' end');
|
|
557
|
-
lines.push(' style sg_changed fill:#fff3e0,stroke:#ff9800');
|
|
558
|
-
}
|
|
589
|
+
addMermaidSubgraph(lines, state, 'added', 'Added', data.added || [], '#e8f5e9', '#4caf50');
|
|
590
|
+
addMermaidSubgraph(lines, state, 'removed', 'Removed', data.removed || [], '#ffebee', '#f44336');
|
|
591
|
+
addMermaidSubgraph(lines, state, 'changed', 'Changed', data.changed || [], '#fff3e0', '#ff9800');
|
|
559
592
|
|
|
560
|
-
const allImpacted = new Map<string, CallerInfo>();
|
|
561
593
|
const impactSources = [...(data.removed || []), ...(data.changed || [])];
|
|
562
|
-
|
|
563
|
-
if (!sym.impact) continue;
|
|
564
|
-
for (const c of sym.impact) {
|
|
565
|
-
const key = `impact::${c.kind}::${c.file}::${c.name}`;
|
|
566
|
-
if (!allImpacted.has(key)) allImpacted.set(key, c);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
594
|
+
const allImpacted = collectImpactedCallers(impactSources);
|
|
569
595
|
|
|
570
596
|
if (allImpacted.size > 0) {
|
|
571
597
|
lines.push(' subgraph sg_impact["Impacted Callers"]');
|
|
572
598
|
for (const [key, c] of allImpacted) {
|
|
573
|
-
const nid =
|
|
599
|
+
const nid = mermaidNodeId(state, key);
|
|
574
600
|
lines.push(` ${nid}["[${kindIcon(c.kind)}] ${c.name}"]`);
|
|
575
601
|
}
|
|
576
602
|
lines.push(' end');
|
|
@@ -583,8 +609,8 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
|
|
|
583
609
|
const symKey = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
|
|
584
610
|
for (const c of sym.impact) {
|
|
585
611
|
const callerKey = `impact::${c.kind}::${c.file}::${c.name}`;
|
|
586
|
-
if (
|
|
587
|
-
lines.push(` ${
|
|
612
|
+
if (state.map.has(symKey) && state.map.has(callerKey)) {
|
|
613
|
+
lines.push(` ${state.map.get(symKey)} -.-> ${state.map.get(callerKey)}`);
|
|
588
614
|
}
|
|
589
615
|
}
|
|
590
616
|
}
|
|
@@ -86,10 +86,7 @@ interface DriftResult {
|
|
|
86
86
|
driftScore: number;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function
|
|
90
|
-
communities: CommunityObject[],
|
|
91
|
-
communityDirs: Map<number, Set<string>>,
|
|
92
|
-
): DriftResult {
|
|
89
|
+
function buildDirToCommunities(communityDirs: Map<number, Set<string>>): Map<string, Set<number>> {
|
|
93
90
|
const dirToCommunities = new Map<string, Set<number>>();
|
|
94
91
|
for (const [cid, dirs] of communityDirs) {
|
|
95
92
|
for (const dir of dirs) {
|
|
@@ -97,20 +94,28 @@ function analyzeDrift(
|
|
|
97
94
|
dirToCommunities.get(dir)!.add(cid);
|
|
98
95
|
}
|
|
99
96
|
}
|
|
97
|
+
return dirToCommunities;
|
|
98
|
+
}
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
function findSplitCandidates(
|
|
101
|
+
dirToCommunities: Map<string, Set<number>>,
|
|
102
|
+
): DriftResult['splitCandidates'] {
|
|
103
|
+
const candidates: DriftResult['splitCandidates'] = [];
|
|
102
104
|
for (const [dir, cids] of dirToCommunities) {
|
|
103
105
|
if (cids.size >= 2) {
|
|
104
|
-
|
|
106
|
+
candidates.push({ directory: dir, communityCount: cids.size });
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
candidates.sort((a, b) => b.communityCount - a.communityCount);
|
|
110
|
+
return candidates;
|
|
111
|
+
}
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
function findMergeCandidates(communities: CommunityObject[]): DriftResult['mergeCandidates'] {
|
|
114
|
+
const candidates: DriftResult['mergeCandidates'] = [];
|
|
110
115
|
for (const c of communities) {
|
|
111
116
|
const dirCount = Object.keys(c.directories).length;
|
|
112
117
|
if (dirCount >= 2) {
|
|
113
|
-
|
|
118
|
+
candidates.push({
|
|
114
119
|
communityId: c.id,
|
|
115
120
|
size: c.size,
|
|
116
121
|
directoryCount: dirCount,
|
|
@@ -118,7 +123,17 @@ function analyzeDrift(
|
|
|
118
123
|
});
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
|
-
|
|
126
|
+
candidates.sort((a, b) => b.directoryCount - a.directoryCount);
|
|
127
|
+
return candidates;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function analyzeDrift(
|
|
131
|
+
communities: CommunityObject[],
|
|
132
|
+
communityDirs: Map<number, Set<string>>,
|
|
133
|
+
): DriftResult {
|
|
134
|
+
const dirToCommunities = buildDirToCommunities(communityDirs);
|
|
135
|
+
const splitCandidates = findSplitCandidates(dirToCommunities);
|
|
136
|
+
const mergeCandidates = findMergeCandidates(communities);
|
|
122
137
|
|
|
123
138
|
const totalDirs = dirToCommunities.size;
|
|
124
139
|
const splitRatio = totalDirs > 0 ? splitCandidates.length / totalDirs : 0;
|
|
@@ -109,6 +109,163 @@ export const computeMaintainabilityIndex = _computeMaintainabilityIndex;
|
|
|
109
109
|
|
|
110
110
|
// ─── Algorithm: Single-Traversal DFS ──────────────────────────────────────
|
|
111
111
|
|
|
112
|
+
interface ComplexityAccumulator {
|
|
113
|
+
cognitive: number;
|
|
114
|
+
cyclomatic: number;
|
|
115
|
+
maxNesting: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type WalkFn = (n: TreeSitterNode | null, level: number, isTop: boolean) => void;
|
|
119
|
+
|
|
120
|
+
/** Walk all children at the given nesting level. */
|
|
121
|
+
function walkChildren(node: TreeSitterNode, nestingLevel: number, walkFn: WalkFn): void {
|
|
122
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
123
|
+
walkFn(node.child(i), nestingLevel, false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Handle logical operators in binary expressions. Returns true if handled. */
|
|
128
|
+
function handleLogicalOperator(
|
|
129
|
+
node: TreeSitterNode,
|
|
130
|
+
type: string,
|
|
131
|
+
rules: ComplexityRules,
|
|
132
|
+
acc: ComplexityAccumulator,
|
|
133
|
+
nestingLevel: number,
|
|
134
|
+
walkFn: WalkFn,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (type !== rules.logicalNodeType) return false;
|
|
137
|
+
|
|
138
|
+
const op = node.child(1)?.type;
|
|
139
|
+
if (!op || !rules.logicalOperators.has(op)) return false;
|
|
140
|
+
|
|
141
|
+
acc.cyclomatic++;
|
|
142
|
+
|
|
143
|
+
// Cognitive: +1 only when operator changes from the previous sibling sequence
|
|
144
|
+
const parent = node.parent;
|
|
145
|
+
const sameSequence = parent?.type === rules.logicalNodeType && parent.child(1)?.type === op;
|
|
146
|
+
if (!sameSequence) acc.cognitive++;
|
|
147
|
+
|
|
148
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Handle else clause wrapping an if (Pattern A: JS/C#/Rust). Returns true if handled. */
|
|
153
|
+
function handleElseClause(
|
|
154
|
+
node: TreeSitterNode,
|
|
155
|
+
type: string,
|
|
156
|
+
rules: ComplexityRules,
|
|
157
|
+
acc: ComplexityAccumulator,
|
|
158
|
+
nestingLevel: number,
|
|
159
|
+
walkFn: WalkFn,
|
|
160
|
+
): boolean {
|
|
161
|
+
if (!rules.elseNodeType || type !== rules.elseNodeType) return false;
|
|
162
|
+
|
|
163
|
+
const firstChild = node.namedChild(0);
|
|
164
|
+
if (firstChild && firstChild.type === rules.ifNodeType) {
|
|
165
|
+
// else-if: the if_statement child handles its own increment
|
|
166
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
// Plain else
|
|
170
|
+
acc.cognitive++;
|
|
171
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Detect and handle else-if patterns (Patterns A, B, C). Returns true if handled. */
|
|
176
|
+
function handleElseIf(
|
|
177
|
+
node: TreeSitterNode,
|
|
178
|
+
type: string,
|
|
179
|
+
rules: ComplexityRules,
|
|
180
|
+
acc: ComplexityAccumulator,
|
|
181
|
+
nestingLevel: number,
|
|
182
|
+
walkFn: WalkFn,
|
|
183
|
+
): boolean {
|
|
184
|
+
// Pattern B: explicit elif node (Python/Ruby/PHP)
|
|
185
|
+
if (rules.elifNodeType && type === rules.elifNodeType) {
|
|
186
|
+
acc.cognitive++;
|
|
187
|
+
acc.cyclomatic++;
|
|
188
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Detect else-if via Pattern A or C
|
|
193
|
+
if (type === rules.ifNodeType) {
|
|
194
|
+
let isElseIf = false;
|
|
195
|
+
if (rules.elseViaAlternative) {
|
|
196
|
+
// Pattern C (Go/Java): if_statement is the alternative of parent if_statement
|
|
197
|
+
isElseIf =
|
|
198
|
+
node.parent?.type === rules.ifNodeType &&
|
|
199
|
+
node.parent.childForFieldName('alternative')?.id === node.id;
|
|
200
|
+
} else if (rules.elseNodeType) {
|
|
201
|
+
// Pattern A (JS/C#/Rust): if_statement inside else_clause
|
|
202
|
+
isElseIf = node.parent?.type === rules.elseNodeType;
|
|
203
|
+
}
|
|
204
|
+
if (isElseIf) {
|
|
205
|
+
acc.cognitive++;
|
|
206
|
+
acc.cyclomatic++;
|
|
207
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Handle branch/control flow nodes. Returns true if handled. */
|
|
216
|
+
function handleBranchNode(
|
|
217
|
+
node: TreeSitterNode,
|
|
218
|
+
type: string,
|
|
219
|
+
rules: ComplexityRules,
|
|
220
|
+
acc: ComplexityAccumulator,
|
|
221
|
+
nestingLevel: number,
|
|
222
|
+
walkFn: WalkFn,
|
|
223
|
+
): boolean {
|
|
224
|
+
if (!rules.branchNodes.has(type) || node.childCount === 0) return false;
|
|
225
|
+
|
|
226
|
+
if (handleElseClause(node, type, rules, acc, nestingLevel, walkFn)) return true;
|
|
227
|
+
if (handleElseIf(node, type, rules, acc, nestingLevel, walkFn)) return true;
|
|
228
|
+
|
|
229
|
+
// Regular branch node
|
|
230
|
+
acc.cognitive += 1 + nestingLevel; // structural + nesting
|
|
231
|
+
acc.cyclomatic++;
|
|
232
|
+
|
|
233
|
+
// Switch-like nodes don't add cyclomatic themselves (cases do)
|
|
234
|
+
if (rules.switchLikeNodes?.has(type)) {
|
|
235
|
+
acc.cyclomatic--;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (rules.nestingNodes.has(type)) {
|
|
239
|
+
walkChildren(node, nestingLevel + 1, walkFn);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Handle Pattern C plain else: block is the alternative of an if_statement (Go/Java). */
|
|
247
|
+
function handlePatternCElse(
|
|
248
|
+
node: TreeSitterNode,
|
|
249
|
+
type: string,
|
|
250
|
+
rules: ComplexityRules,
|
|
251
|
+
acc: ComplexityAccumulator,
|
|
252
|
+
nestingLevel: number,
|
|
253
|
+
walkFn: WalkFn,
|
|
254
|
+
): boolean {
|
|
255
|
+
if (
|
|
256
|
+
!rules.elseViaAlternative ||
|
|
257
|
+
type === rules.ifNodeType ||
|
|
258
|
+
node.parent?.type !== rules.ifNodeType ||
|
|
259
|
+
node.parent.childForFieldName('alternative')?.id !== node.id
|
|
260
|
+
) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
acc.cognitive++;
|
|
265
|
+
walkChildren(node, nestingLevel, walkFn);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
112
269
|
export function computeFunctionComplexity(
|
|
113
270
|
functionNode: TreeSitterNode,
|
|
114
271
|
language: string,
|
|
@@ -116,158 +273,38 @@ export function computeFunctionComplexity(
|
|
|
116
273
|
const rules = COMPLEXITY_RULES.get(language) as ComplexityRules | undefined;
|
|
117
274
|
if (!rules) return null;
|
|
118
275
|
|
|
119
|
-
|
|
120
|
-
let cyclomatic = 1; // McCabe starts at 1
|
|
121
|
-
let maxNesting = 0;
|
|
276
|
+
const acc: ComplexityAccumulator = { cognitive: 0, cyclomatic: 1, maxNesting: 0 };
|
|
122
277
|
|
|
123
278
|
function walk(node: TreeSitterNode | null, nestingLevel: number, isTopFunction: boolean): void {
|
|
124
279
|
if (!node) return;
|
|
125
280
|
|
|
126
281
|
const type = node.type;
|
|
127
282
|
|
|
128
|
-
|
|
129
|
-
if (nestingLevel > maxNesting) maxNesting = nestingLevel;
|
|
130
|
-
|
|
131
|
-
// Handle logical operators in binary expressions
|
|
132
|
-
if (type === rules?.logicalNodeType) {
|
|
133
|
-
const op = node.child(1)?.type;
|
|
134
|
-
if (op && rules?.logicalOperators.has(op)) {
|
|
135
|
-
// Cyclomatic: +1 for every logical operator
|
|
136
|
-
cyclomatic++;
|
|
137
|
-
|
|
138
|
-
// Cognitive: +1 only when operator changes from the previous sibling sequence
|
|
139
|
-
// Walk up to check if parent is same type with same operator
|
|
140
|
-
const parent = node.parent;
|
|
141
|
-
let sameSequence = false;
|
|
142
|
-
if (parent && parent.type === rules?.logicalNodeType) {
|
|
143
|
-
const parentOp = parent.child(1)?.type;
|
|
144
|
-
if (parentOp === op) {
|
|
145
|
-
sameSequence = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!sameSequence) {
|
|
149
|
-
cognitive++;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Walk children manually to avoid double-counting
|
|
153
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
154
|
-
walk(node.child(i), nestingLevel, false);
|
|
155
|
-
}
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
283
|
+
if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
|
|
159
284
|
|
|
160
|
-
|
|
161
|
-
if (type === rules?.optionalChainType) {
|
|
162
|
-
cyclomatic++;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Handle branch/control flow nodes (skip keyword leaf tokens like Ruby's `if`)
|
|
166
|
-
if (rules?.branchNodes.has(type) && node.childCount > 0) {
|
|
167
|
-
// Pattern A: else clause wraps if (JS/C#/Rust)
|
|
168
|
-
if (rules?.elseNodeType && type === rules?.elseNodeType) {
|
|
169
|
-
const firstChild = node.namedChild(0);
|
|
170
|
-
if (firstChild && firstChild.type === rules?.ifNodeType) {
|
|
171
|
-
// else-if: the if_statement child handles its own increment
|
|
172
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
173
|
-
walk(node.child(i), nestingLevel, false);
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
// Plain else
|
|
178
|
-
cognitive++;
|
|
179
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
180
|
-
walk(node.child(i), nestingLevel, false);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
285
|
+
if (handleLogicalOperator(node, type, rules!, acc, nestingLevel, walk)) return;
|
|
184
286
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
cognitive++;
|
|
188
|
-
cyclomatic++;
|
|
189
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
190
|
-
walk(node.child(i), nestingLevel, false);
|
|
191
|
-
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
287
|
+
// Optional chaining (cyclomatic only)
|
|
288
|
+
if (type === rules!.optionalChainType) acc.cyclomatic++;
|
|
194
289
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (type === rules?.ifNodeType) {
|
|
198
|
-
if (rules?.elseViaAlternative) {
|
|
199
|
-
// Pattern C (Go/Java): if_statement is the alternative of parent if_statement
|
|
200
|
-
isElseIf =
|
|
201
|
-
node.parent?.type === rules?.ifNodeType &&
|
|
202
|
-
node.parent.childForFieldName('alternative')?.id === node.id;
|
|
203
|
-
} else if (rules?.elseNodeType) {
|
|
204
|
-
// Pattern A (JS/C#/Rust): if_statement inside else_clause
|
|
205
|
-
isElseIf = node.parent?.type === rules?.elseNodeType;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
290
|
+
if (handleBranchNode(node, type, rules!, acc, nestingLevel, walk)) return;
|
|
291
|
+
if (handlePatternCElse(node, type, rules!, acc, nestingLevel, walk)) return;
|
|
208
292
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
cyclomatic++;
|
|
212
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
213
|
-
walk(node.child(i), nestingLevel, false);
|
|
214
|
-
}
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
293
|
+
// Case nodes (cyclomatic only, skip keyword leaves)
|
|
294
|
+
if (rules!.caseNodes.has(type) && node.childCount > 0) acc.cyclomatic++;
|
|
217
295
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// Switch-like nodes don't add cyclomatic themselves (cases do)
|
|
223
|
-
if (rules?.switchLikeNodes?.has(type)) {
|
|
224
|
-
cyclomatic--; // Undo the ++ above; cases handle cyclomatic
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (rules?.nestingNodes.has(type)) {
|
|
228
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
229
|
-
walk(node.child(i), nestingLevel + 1, false);
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
|
|
236
|
-
if (
|
|
237
|
-
rules?.elseViaAlternative &&
|
|
238
|
-
type !== rules?.ifNodeType &&
|
|
239
|
-
node.parent?.type === rules?.ifNodeType &&
|
|
240
|
-
node.parent.childForFieldName('alternative')?.id === node.id
|
|
241
|
-
) {
|
|
242
|
-
cognitive++;
|
|
243
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
244
|
-
walk(node.child(i), nestingLevel, false);
|
|
245
|
-
}
|
|
296
|
+
// Nested function definitions (increase nesting)
|
|
297
|
+
if (!isTopFunction && rules!.functionNodes.has(type)) {
|
|
298
|
+
walkChildren(node, nestingLevel + 1, walk);
|
|
246
299
|
return;
|
|
247
300
|
}
|
|
248
301
|
|
|
249
|
-
|
|
250
|
-
if (rules?.caseNodes.has(type) && node.childCount > 0) {
|
|
251
|
-
cyclomatic++;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Handle nested function definitions (increase nesting)
|
|
255
|
-
if (!isTopFunction && rules?.functionNodes.has(type)) {
|
|
256
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
257
|
-
walk(node.child(i), nestingLevel + 1, false);
|
|
258
|
-
}
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Walk children
|
|
263
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
264
|
-
walk(node.child(i), nestingLevel, false);
|
|
265
|
-
}
|
|
302
|
+
walkChildren(node, nestingLevel, walk);
|
|
266
303
|
}
|
|
267
304
|
|
|
268
305
|
walk(functionNode, 0, true);
|
|
269
306
|
|
|
270
|
-
return { cognitive, cyclomatic, maxNesting };
|
|
307
|
+
return { cognitive: acc.cognitive, cyclomatic: acc.cyclomatic, maxNesting: acc.maxNesting };
|
|
271
308
|
}
|
|
272
309
|
|
|
273
310
|
// ─── Merged Single-Pass Computation ───────────────────────────────────────
|