@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
|
@@ -21,6 +21,7 @@ import { performance } from 'node:perf_hooks';
|
|
|
21
21
|
import { bulkNodeIdsByFile } from '../db/index.js';
|
|
22
22
|
import { debug } from '../infrastructure/logger.js';
|
|
23
23
|
import { loadNative } from '../infrastructure/native.js';
|
|
24
|
+
import { toErrorMessage } from '../shared/errors.js';
|
|
24
25
|
import type {
|
|
25
26
|
AnalysisOpts,
|
|
26
27
|
AnalysisTiming,
|
|
@@ -102,12 +103,83 @@ async function getParserModule(): Promise<typeof import('../domain/parser.js')>
|
|
|
102
103
|
|
|
103
104
|
// ─── Native standalone analysis ─────────────────────────────────────────
|
|
104
105
|
|
|
106
|
+
interface NativeAnalysisNeeds {
|
|
107
|
+
complexity: boolean;
|
|
108
|
+
cfg: boolean;
|
|
109
|
+
dataflow: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
/**
|
|
106
113
|
* Try native Rust analysis for files missing complexity/CFG/dataflow data.
|
|
107
114
|
* Reads source from disk, calls the native standalone functions, and stores
|
|
108
|
-
* results directly on definitions/symbols.
|
|
109
|
-
* were fully handled (no remaining gaps except possibly AST store).
|
|
115
|
+
* results directly on definitions/symbols.
|
|
110
116
|
*/
|
|
117
|
+
|
|
118
|
+
/** Determine which native analyses a file still needs. */
|
|
119
|
+
function detectNativeNeeds(
|
|
120
|
+
symbols: ExtractorOutput,
|
|
121
|
+
ext: string,
|
|
122
|
+
langId: string,
|
|
123
|
+
opts: { doComplexity: boolean; doCfg: boolean; doDataflow: boolean },
|
|
124
|
+
): NativeAnalysisNeeds {
|
|
125
|
+
const defs = symbols.definitions || [];
|
|
126
|
+
const langSupportsComplexity = COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(langId);
|
|
127
|
+
const langSupportsCfg = CFG_EXTENSIONS.has(ext) || CFG_RULES.has(langId);
|
|
128
|
+
const langSupportsDataflow = DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(langId);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
complexity:
|
|
132
|
+
opts.doComplexity &&
|
|
133
|
+
langSupportsComplexity &&
|
|
134
|
+
defs.some((d) => hasFuncBody(d) && !d.complexity),
|
|
135
|
+
cfg:
|
|
136
|
+
opts.doCfg &&
|
|
137
|
+
langSupportsCfg &&
|
|
138
|
+
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks)),
|
|
139
|
+
dataflow: opts.doDataflow && !symbols.dataflow && langSupportsDataflow,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Run native analysis passes for a single file. */
|
|
144
|
+
function runNativeFileAnalysis(
|
|
145
|
+
native: NativeAddon,
|
|
146
|
+
source: string,
|
|
147
|
+
absPath: string,
|
|
148
|
+
relPath: string,
|
|
149
|
+
langId: string,
|
|
150
|
+
symbols: ExtractorOutput,
|
|
151
|
+
needs: NativeAnalysisNeeds,
|
|
152
|
+
): void {
|
|
153
|
+
const defs = symbols.definitions || [];
|
|
154
|
+
|
|
155
|
+
if (needs.complexity && native.analyzeComplexity) {
|
|
156
|
+
try {
|
|
157
|
+
const results = native.analyzeComplexity(source, absPath, langId);
|
|
158
|
+
storeNativeComplexityResults(results, defs);
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
debug(`native analyzeComplexity failed for ${relPath}: ${toErrorMessage(err)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (needs.cfg && native.buildCfgAnalysis) {
|
|
165
|
+
try {
|
|
166
|
+
const results = native.buildCfgAnalysis(source, absPath, langId);
|
|
167
|
+
storeNativeCfgResults(results, defs);
|
|
168
|
+
} catch (err: unknown) {
|
|
169
|
+
debug(`native buildCfgAnalysis failed for ${relPath}: ${toErrorMessage(err)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (needs.dataflow && native.extractDataflowAnalysis) {
|
|
174
|
+
try {
|
|
175
|
+
const result = native.extractDataflowAnalysis(source, absPath, langId);
|
|
176
|
+
if (result) symbols.dataflow = result;
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
debug(`native extractDataflowAnalysis failed for ${relPath}: ${toErrorMessage(err)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
111
183
|
function runNativeAnalysis(
|
|
112
184
|
native: NativeAddon,
|
|
113
185
|
fileSymbols: Map<string, ExtractorOutput>,
|
|
@@ -115,68 +187,31 @@ function runNativeAnalysis(
|
|
|
115
187
|
opts: AnalysisOpts,
|
|
116
188
|
extToLang: Map<string, string>,
|
|
117
189
|
): void {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
const optsFlags = {
|
|
191
|
+
doComplexity: opts.complexity !== false,
|
|
192
|
+
doCfg: opts.cfg !== false,
|
|
193
|
+
doDataflow: opts.dataflow !== false,
|
|
194
|
+
};
|
|
121
195
|
|
|
122
196
|
for (const [relPath, symbols] of fileSymbols) {
|
|
123
|
-
if (symbols._tree) continue;
|
|
197
|
+
if (symbols._tree) continue;
|
|
124
198
|
const ext = path.extname(relPath).toLowerCase();
|
|
125
199
|
const langId = symbols._langId || extToLang.get(ext);
|
|
126
200
|
if (!langId) continue;
|
|
127
201
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
const needsComplexity =
|
|
131
|
-
doComplexity &&
|
|
132
|
-
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
133
|
-
defs.some((d) => hasFuncBody(d) && !d.complexity);
|
|
134
|
-
const needsCfg =
|
|
135
|
-
doCfg &&
|
|
136
|
-
CFG_EXTENSIONS.has(ext) &&
|
|
137
|
-
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
|
|
138
|
-
const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
|
|
202
|
+
const needs = detectNativeNeeds(symbols, ext, langId, optsFlags);
|
|
203
|
+
if (!needs.complexity && !needs.cfg && !needs.dataflow) continue;
|
|
139
204
|
|
|
140
|
-
if (!needsComplexity && !needsCfg && !needsDataflow) continue;
|
|
141
|
-
|
|
142
|
-
// Read source from disk
|
|
143
205
|
const absPath = path.join(rootDir, relPath);
|
|
144
206
|
let source: string;
|
|
145
207
|
try {
|
|
146
208
|
source = fs.readFileSync(absPath, 'utf-8');
|
|
147
|
-
} catch {
|
|
209
|
+
} catch (e) {
|
|
210
|
+
debug(`runNativeAnalysis: failed to read ${relPath}: ${toErrorMessage(e)}`);
|
|
148
211
|
continue;
|
|
149
212
|
}
|
|
150
213
|
|
|
151
|
-
|
|
152
|
-
if (needsComplexity && native.analyzeComplexity) {
|
|
153
|
-
try {
|
|
154
|
-
const results = native.analyzeComplexity(source, absPath);
|
|
155
|
-
storeNativeComplexityResults(results, defs);
|
|
156
|
-
} catch (err: unknown) {
|
|
157
|
-
debug(`native analyzeComplexity failed for ${relPath}: ${(err as Error).message}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// CFG
|
|
162
|
-
if (needsCfg && native.buildCfgAnalysis) {
|
|
163
|
-
try {
|
|
164
|
-
const results = native.buildCfgAnalysis(source, absPath);
|
|
165
|
-
storeNativeCfgResults(results, defs);
|
|
166
|
-
} catch (err: unknown) {
|
|
167
|
-
debug(`native buildCfgAnalysis failed for ${relPath}: ${(err as Error).message}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Dataflow
|
|
172
|
-
if (needsDataflow && native.extractDataflowAnalysis) {
|
|
173
|
-
try {
|
|
174
|
-
const result = native.extractDataflowAnalysis(source, absPath);
|
|
175
|
-
if (result) symbols.dataflow = result;
|
|
176
|
-
} catch (err: unknown) {
|
|
177
|
-
debug(`native extractDataflowAnalysis failed for ${relPath}: ${(err as Error).message}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
214
|
+
runNativeFileAnalysis(native, source, absPath, relPath, langId, symbols, needs);
|
|
180
215
|
}
|
|
181
216
|
}
|
|
182
217
|
|
|
@@ -207,6 +242,12 @@ function storeNativeComplexityResults(
|
|
|
207
242
|
maxNesting: c.maxNesting,
|
|
208
243
|
halstead: c.halstead
|
|
209
244
|
? {
|
|
245
|
+
n1: c.halstead.n1,
|
|
246
|
+
n2: c.halstead.n2,
|
|
247
|
+
bigN1: c.halstead.bigN1,
|
|
248
|
+
bigN2: c.halstead.bigN2,
|
|
249
|
+
vocabulary: c.halstead.vocabulary,
|
|
250
|
+
length: c.halstead.length,
|
|
210
251
|
volume: c.halstead.volume,
|
|
211
252
|
difficulty: c.halstead.difficulty,
|
|
212
253
|
effort: c.halstead.effort,
|
|
@@ -222,6 +263,25 @@ function storeNativeComplexityResults(
|
|
|
222
263
|
}
|
|
223
264
|
}
|
|
224
265
|
|
|
266
|
+
/** Override a definition's cyclomatic complexity with a CFG-derived value and recompute MI. */
|
|
267
|
+
function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void {
|
|
268
|
+
if (!def.complexity) return;
|
|
269
|
+
if (cfgCyclomatic <= 0) {
|
|
270
|
+
debug(`overrideCyclomaticFromCfg: skipping ${def.name} — cfgCyclomatic=${cfgCyclomatic}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
def.complexity.cyclomatic = cfgCyclomatic;
|
|
274
|
+
const { loc, halstead } = def.complexity;
|
|
275
|
+
const volume = halstead ? halstead.volume : 0;
|
|
276
|
+
const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
277
|
+
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
278
|
+
volume,
|
|
279
|
+
cfgCyclomatic,
|
|
280
|
+
loc?.sloc ?? 0,
|
|
281
|
+
commentRatio,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
225
285
|
/** Store native CFG results on definitions, matched by line number. */
|
|
226
286
|
function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
|
|
227
287
|
const byLine = new Map<number, NativeFunctionCfgResult[]>();
|
|
@@ -248,19 +308,43 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
|
|
|
248
308
|
|
|
249
309
|
// Override complexity cyclomatic with CFG-derived value
|
|
250
310
|
const { edges, blocks } = match.cfg;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
311
|
+
if (edges && blocks) {
|
|
312
|
+
overrideCyclomaticFromCfg(def, edges.length - blocks.length + 2);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── CFG cyclomatic reconciliation ──────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Apply CFG-derived cyclomatic override for definitions that already have both
|
|
322
|
+
* `complexity` and `cfg` with blocks/edges but whose cyclomatic was never
|
|
323
|
+
* overridden (e.g., native extractors provide both fields inline, so the
|
|
324
|
+
* normal override path in storeNativeCfgResults / storeCfgResults is skipped).
|
|
325
|
+
*/
|
|
326
|
+
/** Type guard for cfg objects with blocks and edges arrays. */
|
|
327
|
+
function hasCfgBlocksAndEdges(cfg: unknown): cfg is { blocks: unknown[]; edges: unknown[] } {
|
|
328
|
+
return (
|
|
329
|
+
cfg != null &&
|
|
330
|
+
typeof cfg === 'object' &&
|
|
331
|
+
Array.isArray((cfg as { blocks?: unknown }).blocks) &&
|
|
332
|
+
Array.isArray((cfg as { edges?: unknown }).edges)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void {
|
|
337
|
+
for (const [, symbols] of fileSymbols) {
|
|
338
|
+
const defs = symbols.definitions || [];
|
|
339
|
+
for (const def of defs) {
|
|
340
|
+
if (
|
|
341
|
+
(def.kind === 'function' || def.kind === 'method') &&
|
|
342
|
+
def.complexity &&
|
|
343
|
+
hasCfgBlocksAndEdges(def.cfg)
|
|
344
|
+
) {
|
|
345
|
+
const cfgCyclomatic = Math.max(def.cfg.edges.length - def.cfg.blocks.length + 2, 1);
|
|
346
|
+
if (cfgCyclomatic !== def.complexity.cyclomatic) {
|
|
347
|
+
overrideCyclomaticFromCfg(def, cfgCyclomatic);
|
|
264
348
|
}
|
|
265
349
|
}
|
|
266
350
|
}
|
|
@@ -287,34 +371,22 @@ async function ensureWasmTreesIfNeeded(
|
|
|
287
371
|
const ext = path.extname(relPath).toLowerCase();
|
|
288
372
|
const defs = symbols.definitions || [];
|
|
289
373
|
|
|
290
|
-
// Only consider definitions with a real function body.
|
|
291
|
-
// Interface/type property signatures are extracted as methods but correctly
|
|
292
|
-
// lack complexity/CFG data from the native engine. Exclude them by:
|
|
293
|
-
// 1. Single-line span (endLine === line) — type property on one line
|
|
294
|
-
// 2. Dotted names (e.g. "Interface.prop") — child definitions of types
|
|
295
|
-
const hasFuncBody = (d: {
|
|
296
|
-
name: string;
|
|
297
|
-
kind: string;
|
|
298
|
-
line: number;
|
|
299
|
-
endLine?: number | null;
|
|
300
|
-
}) =>
|
|
301
|
-
(d.kind === 'function' || d.kind === 'method') &&
|
|
302
|
-
d.line > 0 &&
|
|
303
|
-
d.endLine != null &&
|
|
304
|
-
d.endLine > d.line &&
|
|
305
|
-
!d.name.includes('.');
|
|
306
|
-
|
|
307
374
|
// AST: need tree when native didn't provide non-call astNodes
|
|
308
|
-
const
|
|
375
|
+
const lid = symbols._langId || '';
|
|
376
|
+
const needsAst =
|
|
377
|
+
doAst &&
|
|
378
|
+
!Array.isArray(symbols.astNodes) &&
|
|
379
|
+
(WALK_EXTENSIONS.has(ext) || AST_TYPE_MAPS.has(lid));
|
|
309
380
|
const needsComplexity =
|
|
310
381
|
doComplexity &&
|
|
311
|
-
COMPLEXITY_EXTENSIONS.has(ext) &&
|
|
382
|
+
(COMPLEXITY_EXTENSIONS.has(ext) || COMPLEXITY_RULES.has(lid)) &&
|
|
312
383
|
defs.some((d) => hasFuncBody(d) && !d.complexity);
|
|
313
384
|
const needsCfg =
|
|
314
385
|
doCfg &&
|
|
315
|
-
CFG_EXTENSIONS.has(ext) &&
|
|
386
|
+
(CFG_EXTENSIONS.has(ext) || CFG_RULES.has(lid)) &&
|
|
316
387
|
defs.some((d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks));
|
|
317
|
-
const needsDataflow =
|
|
388
|
+
const needsDataflow =
|
|
389
|
+
doDataflow && !symbols.dataflow && (DATAFLOW_EXTENSIONS.has(ext) || DATAFLOW_RULES.has(lid));
|
|
318
390
|
|
|
319
391
|
if (needsAst || needsComplexity || needsCfg || needsDataflow) {
|
|
320
392
|
needsWasmTrees = true;
|
|
@@ -327,7 +399,7 @@ async function ensureWasmTreesIfNeeded(
|
|
|
327
399
|
const { ensureWasmTrees } = await getParserModule();
|
|
328
400
|
await ensureWasmTrees(fileSymbols, rootDir);
|
|
329
401
|
} catch (err: unknown) {
|
|
330
|
-
debug(`ensureWasmTrees failed: ${(err
|
|
402
|
+
debug(`ensureWasmTrees failed: ${toErrorMessage(err)}`);
|
|
331
403
|
}
|
|
332
404
|
}
|
|
333
405
|
}
|
|
@@ -396,9 +468,9 @@ function setupComplexityVisitorForFile(
|
|
|
396
468
|
}
|
|
397
469
|
|
|
398
470
|
/** Set up CFG visitor if any definitions need WASM CFG analysis. */
|
|
399
|
-
function setupCfgVisitorForFile(defs: Definition[], langId: string
|
|
471
|
+
function setupCfgVisitorForFile(defs: Definition[], langId: string): Visitor | null {
|
|
400
472
|
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
401
|
-
if (!cfgRulesForLang
|
|
473
|
+
if (!cfgRulesForLang) return null;
|
|
402
474
|
|
|
403
475
|
const needsWasmCfg = defs.some(
|
|
404
476
|
(d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
|
|
@@ -432,12 +504,12 @@ function setupVisitors(
|
|
|
432
504
|
opts.complexity !== false ? setupComplexityVisitorForFile(defs, langId, walkerOpts) : null;
|
|
433
505
|
if (complexityVisitor) visitors.push(complexityVisitor);
|
|
434
506
|
|
|
435
|
-
const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId
|
|
507
|
+
const cfgVisitor = opts.cfg !== false ? setupCfgVisitorForFile(defs, langId) : null;
|
|
436
508
|
if (cfgVisitor) visitors.push(cfgVisitor);
|
|
437
509
|
|
|
438
510
|
let dataflowVisitor: Visitor | null = null;
|
|
439
511
|
const dfRules = DATAFLOW_RULES.get(langId);
|
|
440
|
-
if (opts.dataflow !== false && dfRules &&
|
|
512
|
+
if (opts.dataflow !== false && dfRules && !symbols.dataflow) {
|
|
441
513
|
dataflowVisitor = createDataflowVisitor(dfRules);
|
|
442
514
|
visitors.push(dataflowVisitor);
|
|
443
515
|
}
|
|
@@ -510,17 +582,8 @@ function storeCfgResults(results: WalkResults, defs: Definition[]): void {
|
|
|
510
582
|
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
|
|
511
583
|
|
|
512
584
|
// Override complexity's cyclomatic with CFG-derived value (single source of truth)
|
|
513
|
-
if (
|
|
514
|
-
def
|
|
515
|
-
const { loc, halstead } = def.complexity;
|
|
516
|
-
const volume = halstead ? halstead.volume : 0;
|
|
517
|
-
const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
518
|
-
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
|
|
519
|
-
volume,
|
|
520
|
-
cfgResult.cyclomatic,
|
|
521
|
-
loc?.sloc ?? 0,
|
|
522
|
-
commentRatio,
|
|
523
|
-
);
|
|
585
|
+
if (cfgResult.cyclomatic != null) {
|
|
586
|
+
overrideCyclomaticFromCfg(def, cfgResult.cyclomatic);
|
|
524
587
|
}
|
|
525
588
|
}
|
|
526
589
|
}
|
|
@@ -542,7 +605,7 @@ async function delegateToBuildFunctions(
|
|
|
542
605
|
const { buildAstNodes } = await import('../features/ast.js');
|
|
543
606
|
await buildAstNodes(db, fileSymbols as Map<string, any>, rootDir, engineOpts);
|
|
544
607
|
} catch (err: unknown) {
|
|
545
|
-
debug(`buildAstNodes failed: ${(err
|
|
608
|
+
debug(`buildAstNodes failed: ${toErrorMessage(err)}`);
|
|
546
609
|
}
|
|
547
610
|
timing.astMs = performance.now() - t0;
|
|
548
611
|
}
|
|
@@ -553,7 +616,7 @@ async function delegateToBuildFunctions(
|
|
|
553
616
|
const { buildComplexityMetrics } = await import('../features/complexity.js');
|
|
554
617
|
await buildComplexityMetrics(db, fileSymbols as Map<string, any>, rootDir, engineOpts);
|
|
555
618
|
} catch (err: unknown) {
|
|
556
|
-
debug(`buildComplexityMetrics failed: ${(err
|
|
619
|
+
debug(`buildComplexityMetrics failed: ${toErrorMessage(err)}`);
|
|
557
620
|
}
|
|
558
621
|
timing.complexityMs = performance.now() - t0;
|
|
559
622
|
}
|
|
@@ -564,7 +627,7 @@ async function delegateToBuildFunctions(
|
|
|
564
627
|
const { buildCFGData } = await import('../features/cfg.js');
|
|
565
628
|
await buildCFGData(db, fileSymbols, rootDir, engineOpts);
|
|
566
629
|
} catch (err: unknown) {
|
|
567
|
-
debug(`buildCFGData failed: ${(err
|
|
630
|
+
debug(`buildCFGData failed: ${toErrorMessage(err)}`);
|
|
568
631
|
}
|
|
569
632
|
timing.cfgMs = performance.now() - t0;
|
|
570
633
|
}
|
|
@@ -575,7 +638,7 @@ async function delegateToBuildFunctions(
|
|
|
575
638
|
const { buildDataflowEdges } = await import('../features/dataflow.js');
|
|
576
639
|
await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
|
|
577
640
|
} catch (err: unknown) {
|
|
578
|
-
debug(`buildDataflowEdges failed: ${(err
|
|
641
|
+
debug(`buildDataflowEdges failed: ${toErrorMessage(err)}`);
|
|
579
642
|
}
|
|
580
643
|
timing.dataflowMs = performance.now() - t0;
|
|
581
644
|
}
|
|
@@ -644,6 +707,15 @@ export async function runAnalyses(
|
|
|
644
707
|
|
|
645
708
|
timing._unifiedWalkMs = performance.now() - t0walk;
|
|
646
709
|
|
|
710
|
+
// Reconcile: apply CFG-derived cyclomatic override for any definitions that have
|
|
711
|
+
// both precomputed complexity and CFG data but whose cyclomatic was never overridden.
|
|
712
|
+
// This closes a parity gap where native extractors provide both fields inline but
|
|
713
|
+
// the override step (storeNativeCfgResults / storeCfgResults) is skipped because
|
|
714
|
+
// detectNativeNeeds sees both as already present.
|
|
715
|
+
if (doComplexity && doCfg) {
|
|
716
|
+
reconcileCfgCyclomatic(fileSymbols);
|
|
717
|
+
}
|
|
718
|
+
|
|
647
719
|
// Delegate to buildXxx functions for DB writes + native fallback
|
|
648
720
|
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
|
|
649
721
|
|
|
@@ -9,6 +9,16 @@ import type { HalsteadDerivedMetrics, LOCMetrics, TreeSitterNode } from '../type
|
|
|
9
9
|
|
|
10
10
|
// ─── Halstead Derived Metrics ─────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
+
/** Halstead delivered-bugs denominator (industry standard: V / 3000). */
|
|
13
|
+
const HALSTEAD_BUGS_DIVISOR = 3000;
|
|
14
|
+
|
|
15
|
+
/** Sum all values in a count map. */
|
|
16
|
+
function sumCounts(map: Map<string, number>): number {
|
|
17
|
+
let total = 0;
|
|
18
|
+
for (const c of map.values()) total += c;
|
|
19
|
+
return total;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Compute Halstead derived metrics from raw operator/operand counts.
|
|
14
24
|
*
|
|
@@ -22,17 +32,15 @@ export function computeHalsteadDerived(
|
|
|
22
32
|
): HalsteadDerivedMetrics {
|
|
23
33
|
const n1 = operators.size;
|
|
24
34
|
const n2 = operands.size;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let bigN2 = 0;
|
|
28
|
-
for (const c of operands.values()) bigN2 += c;
|
|
35
|
+
const bigN1 = sumCounts(operators);
|
|
36
|
+
const bigN2 = sumCounts(operands);
|
|
29
37
|
|
|
30
38
|
const vocabulary = n1 + n2;
|
|
31
39
|
const length = bigN1 + bigN2;
|
|
32
40
|
const volume = vocabulary > 0 ? length * Math.log2(vocabulary) : 0;
|
|
33
41
|
const difficulty = n2 > 0 ? (n1 / 2) * (bigN2 / n2) : 0;
|
|
34
42
|
const effort = difficulty * volume;
|
|
35
|
-
const bugs = volume /
|
|
43
|
+
const bugs = volume / HALSTEAD_BUGS_DIVISOR;
|
|
36
44
|
|
|
37
45
|
return {
|
|
38
46
|
n1,
|
|
@@ -97,10 +105,20 @@ export function computeLOCMetrics(functionNode: TreeSitterNode, language?: strin
|
|
|
97
105
|
// ─── Maintainability Index ────────────────────────────────────────────────
|
|
98
106
|
|
|
99
107
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Original SEI formula: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
|
|
108
|
+
* SEI Maintainability Index formula coefficients.
|
|
109
|
+
* Original: MI = 171 - 5.2*ln(V) - 0.23*G - 16.2*ln(LOC) + 50*sin(sqrt(2.4*CM))
|
|
103
110
|
* Microsoft normalization: max(0, min(100, MI * 100/171))
|
|
111
|
+
*/
|
|
112
|
+
const MI_BASE = 171;
|
|
113
|
+
const MI_VOLUME_COEFF = 5.2;
|
|
114
|
+
const MI_CYCLOMATIC_COEFF = 0.23;
|
|
115
|
+
const MI_LOC_COEFF = 16.2;
|
|
116
|
+
const MI_COMMENT_AMPLITUDE = 50;
|
|
117
|
+
const MI_COMMENT_SCALE = 2.4;
|
|
118
|
+
const MI_NORMALIZE_SCALE = 100;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compute normalized Maintainability Index (0-100 scale).
|
|
104
122
|
*
|
|
105
123
|
* @param {number} volume - Halstead volume
|
|
106
124
|
* @param {number} cyclomatic - Cyclomatic complexity
|
|
@@ -117,12 +135,16 @@ export function computeMaintainabilityIndex(
|
|
|
117
135
|
const safeVolume = Math.max(volume, 1);
|
|
118
136
|
const safeSLOC = Math.max(sloc, 1);
|
|
119
137
|
|
|
120
|
-
let mi =
|
|
138
|
+
let mi =
|
|
139
|
+
MI_BASE -
|
|
140
|
+
MI_VOLUME_COEFF * Math.log(safeVolume) -
|
|
141
|
+
MI_CYCLOMATIC_COEFF * cyclomatic -
|
|
142
|
+
MI_LOC_COEFF * Math.log(safeSLOC);
|
|
121
143
|
|
|
122
144
|
if (commentRatio != null && commentRatio > 0) {
|
|
123
|
-
mi +=
|
|
145
|
+
mi += MI_COMMENT_AMPLITUDE * Math.sin(Math.sqrt(MI_COMMENT_SCALE * commentRatio));
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
const normalized = Math.max(0, Math.min(
|
|
148
|
+
const normalized = Math.max(0, Math.min(MI_NORMALIZE_SCALE, (mi * MI_NORMALIZE_SCALE) / MI_BASE));
|
|
127
149
|
return +normalized.toFixed(1);
|
|
128
150
|
}
|
|
@@ -150,6 +150,38 @@ export function makeDataflowRules(overrides: Partial<DataflowRulesConfig>): Data
|
|
|
150
150
|
|
|
151
151
|
// ─── AST Helpers ──────────────────────────────────────────────────────────
|
|
152
152
|
|
|
153
|
+
/** Compute the span (row count) of a tree-sitter node. */
|
|
154
|
+
function nodeSpan(node: TreeSitterNode): number {
|
|
155
|
+
return node.endPosition.row - node.startPosition.row;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Recursively search for the narrowest function node at the target line.
|
|
160
|
+
*/
|
|
161
|
+
function searchFunctionNode(
|
|
162
|
+
node: TreeSitterNode,
|
|
163
|
+
targetStart: number,
|
|
164
|
+
functionNodeTypes: Set<string>,
|
|
165
|
+
best: TreeSitterNode | null,
|
|
166
|
+
): TreeSitterNode | null {
|
|
167
|
+
const nodeStart = node.startPosition.row;
|
|
168
|
+
const nodeEnd = node.endPosition.row;
|
|
169
|
+
|
|
170
|
+
// Prune branches outside range
|
|
171
|
+
if (nodeEnd < targetStart || nodeStart > targetStart + 1) return best;
|
|
172
|
+
|
|
173
|
+
if (functionNodeTypes.has(node.type) && nodeStart === targetStart) {
|
|
174
|
+
if (!best || nodeSpan(node) < nodeSpan(best)) {
|
|
175
|
+
best = node;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
180
|
+
best = searchFunctionNode(node.child(i)!, targetStart, functionNodeTypes, best);
|
|
181
|
+
}
|
|
182
|
+
return best;
|
|
183
|
+
}
|
|
184
|
+
|
|
153
185
|
export function findFunctionNode(
|
|
154
186
|
rootNode: TreeSitterNode,
|
|
155
187
|
startLine: number,
|
|
@@ -158,30 +190,7 @@ export function findFunctionNode(
|
|
|
158
190
|
): TreeSitterNode | null {
|
|
159
191
|
// tree-sitter lines are 0-indexed
|
|
160
192
|
const targetStart = startLine - 1;
|
|
161
|
-
|
|
162
|
-
let best: TreeSitterNode | null = null;
|
|
163
|
-
|
|
164
|
-
function search(node: TreeSitterNode): void {
|
|
165
|
-
const nodeStart = node.startPosition.row;
|
|
166
|
-
const nodeEnd = node.endPosition.row;
|
|
167
|
-
|
|
168
|
-
// Prune branches outside range
|
|
169
|
-
if (nodeEnd < targetStart || nodeStart > targetStart + 1) return;
|
|
170
|
-
|
|
171
|
-
if (rules.functionNodes.has(node.type) && nodeStart === targetStart) {
|
|
172
|
-
// Found a function node at the right position — pick it
|
|
173
|
-
if (!best || nodeEnd - nodeStart < best.endPosition.row - best.startPosition.row) {
|
|
174
|
-
best = node;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
179
|
-
search(node.child(i)!);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
search(rootNode);
|
|
184
|
-
return best;
|
|
193
|
+
return searchFunctionNode(rootNode, targetStart, rules.functionNodes, null);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
// ─── Extension / Language Mapping ─────────────────────────────────────────
|
|
@@ -88,6 +88,41 @@ export function extractParams(
|
|
|
88
88
|
return result;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** Extract names from a rest parameter (e.g. `...args`). */
|
|
92
|
+
function extractRestParamNames(node: TreeSitterNode, rules: LanguageRules): string[] {
|
|
93
|
+
const nameNode = node.childForFieldName('name');
|
|
94
|
+
if (nameNode) return [nameNode.text];
|
|
95
|
+
for (const child of node.namedChildren) {
|
|
96
|
+
if (child.type === rules.paramIdentifier) return [child.text];
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Extract names from an object destructuring pattern (e.g. `{ a, b: c }`). */
|
|
102
|
+
function extractObjectDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
|
|
103
|
+
const names: string[] = [];
|
|
104
|
+
for (const child of node.namedChildren) {
|
|
105
|
+
if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
|
|
106
|
+
names.push(child.text);
|
|
107
|
+
} else if (rules.pairPatternType && child.type === rules.pairPatternType) {
|
|
108
|
+
const value = child.childForFieldName('value');
|
|
109
|
+
if (value) names.push(...extractParamNames(value, rules));
|
|
110
|
+
} else if (rules.restParamType && child.type === rules.restParamType) {
|
|
111
|
+
names.push(...extractParamNames(child, rules));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Extract names from an array destructuring pattern (e.g. `[a, b]`). */
|
|
118
|
+
function extractArrayDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
|
|
119
|
+
const names: string[] = [];
|
|
120
|
+
for (const child of node.namedChildren) {
|
|
121
|
+
names.push(...extractParamNames(child, rules));
|
|
122
|
+
}
|
|
123
|
+
return names;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/**
|
|
92
127
|
* Extract parameter names from a single parameter node.
|
|
93
128
|
*/
|
|
@@ -113,35 +148,15 @@ export function extractParamNames(node: TreeSitterNode | null, rules: LanguageRu
|
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
if (rules.restParamType && t === rules.restParamType) {
|
|
116
|
-
|
|
117
|
-
if (nameNode) return [nameNode.text];
|
|
118
|
-
for (const child of node.namedChildren) {
|
|
119
|
-
if (child.type === rules.paramIdentifier) return [child.text];
|
|
120
|
-
}
|
|
121
|
-
return [];
|
|
151
|
+
return extractRestParamNames(node, rules);
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
if (rules.objectDestructType && t === rules.objectDestructType) {
|
|
125
|
-
|
|
126
|
-
for (const child of node.namedChildren) {
|
|
127
|
-
if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
|
|
128
|
-
names.push(child.text);
|
|
129
|
-
} else if (rules.pairPatternType && child.type === rules.pairPatternType) {
|
|
130
|
-
const value = child.childForFieldName('value');
|
|
131
|
-
if (value) names.push(...extractParamNames(value, rules));
|
|
132
|
-
} else if (rules.restParamType && child.type === rules.restParamType) {
|
|
133
|
-
names.push(...extractParamNames(child, rules));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return names;
|
|
155
|
+
return extractObjectDestructNames(node, rules);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
if (rules.arrayDestructType && t === rules.arrayDestructType) {
|
|
140
|
-
|
|
141
|
-
for (const child of node.namedChildren) {
|
|
142
|
-
names.push(...extractParamNames(child, rules));
|
|
143
|
-
}
|
|
144
|
-
return names;
|
|
159
|
+
return extractArrayDestructNames(node, rules);
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
return [];
|
|
@@ -155,6 +170,19 @@ export function isIdent(nodeType: string, rules: LanguageRules): boolean {
|
|
|
155
170
|
return rules.extraIdentifierTypes ? rules.extraIdentifierTypes.has(nodeType) : false;
|
|
156
171
|
}
|
|
157
172
|
|
|
173
|
+
/** Resolve callee name from an optional chain node (e.g. `obj?.method()`). */
|
|
174
|
+
function resolveOptionalChainCallee(fn: TreeSitterNode, rules: LanguageRules): string | null {
|
|
175
|
+
const target = fn.namedChildren[0];
|
|
176
|
+
if (!target) return null;
|
|
177
|
+
if (target.type === rules.memberNode) {
|
|
178
|
+
const prop = target.childForFieldName(rules.memberPropertyField);
|
|
179
|
+
return prop ? prop.text : null;
|
|
180
|
+
}
|
|
181
|
+
if (target.type === 'identifier') return target.text;
|
|
182
|
+
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
183
|
+
return prop ? prop.text : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
158
186
|
/**
|
|
159
187
|
* Resolve the name a call expression is calling using rules.
|
|
160
188
|
*/
|
|
@@ -170,15 +198,7 @@ export function resolveCalleeName(callNode: TreeSitterNode, rules: LanguageRules
|
|
|
170
198
|
return prop ? prop.text : null;
|
|
171
199
|
}
|
|
172
200
|
if (rules.optionalChainNode && fn.type === rules.optionalChainNode) {
|
|
173
|
-
|
|
174
|
-
if (!target) return null;
|
|
175
|
-
if (target.type === rules.memberNode) {
|
|
176
|
-
const prop = target.childForFieldName(rules.memberPropertyField);
|
|
177
|
-
return prop ? prop.text : null;
|
|
178
|
-
}
|
|
179
|
-
if (target.type === 'identifier') return target.text;
|
|
180
|
-
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
181
|
-
return prop ? prop.text : null;
|
|
201
|
+
return resolveOptionalChainCallee(fn, rules);
|
|
182
202
|
}
|
|
183
203
|
return null;
|
|
184
204
|
}
|