codedeep-mcp 0.1.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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/config.js +223 -0
- package/dist/git/analyzer.js +177 -0
- package/dist/git/git-service.js +568 -0
- package/dist/git/head-watcher.js +113 -0
- package/dist/git/runner.js +204 -0
- package/dist/index.js +138 -0
- package/dist/indexer/code-index.js +1801 -0
- package/dist/indexer/complexity.js +633 -0
- package/dist/indexer/extractor.js +354 -0
- package/dist/indexer/languages/cpp.js +934 -0
- package/dist/indexer/languages/csharp.js +854 -0
- package/dist/indexer/languages/dart.js +777 -0
- package/dist/indexer/languages/go.js +665 -0
- package/dist/indexer/languages/java.js +507 -0
- package/dist/indexer/languages/kotlin.js +709 -0
- package/dist/indexer/languages/objc.js +397 -0
- package/dist/indexer/languages/php.js +771 -0
- package/dist/indexer/languages/python.js +455 -0
- package/dist/indexer/languages/ruby.js +697 -0
- package/dist/indexer/languages/rust.js +754 -0
- package/dist/indexer/languages/swift.js +691 -0
- package/dist/indexer/languages/typescript.js +485 -0
- package/dist/indexer/parser.js +175 -0
- package/dist/indexer/pipeline.js +342 -0
- package/dist/indexer/scanner.js +279 -0
- package/dist/indexer/watcher.js +353 -0
- package/dist/logger.js +16 -0
- package/dist/server.js +170 -0
- package/dist/tools/common.js +207 -0
- package/dist/tools/find-references.js +224 -0
- package/dist/tools/find-symbol.js +94 -0
- package/dist/tools/get-context.js +370 -0
- package/dist/tools/impact.js +218 -0
- package/dist/tools/overview.js +482 -0
- package/dist/tools/search-structure.js +303 -0
- package/dist/types.js +61 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-objc.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +67 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// Shared C-family boolean-operator reader (TS/JS + Go). One `binary_expression`
|
|
2
|
+
// node covers ALL binary operators, so read the operator TOKEN and count only
|
|
3
|
+
// the short-circuiting logical operators — the C-family boolean trap (`a + b`/
|
|
4
|
+
// `a == b` must NOT increment). VERIFIED against SonarJS source (S1541): its
|
|
5
|
+
// cyclomatic counts `&&`, `||`, AND `??` (all three are one ESTree
|
|
6
|
+
// LogicalExpression with no operator filter); it does NOT count the
|
|
7
|
+
// logical-ASSIGNMENT forms `&&=`/`||=`/`??=` (those are an AssignmentExpression,
|
|
8
|
+
// absent from the cyclomatic switch). tree-sitter groups `??` as a
|
|
9
|
+
// `binary_expression` (op token `??`), so reading the token catches it. Go has
|
|
10
|
+
// no `??` nor logical-assignment, so the `??` entry is simply never hit there
|
|
11
|
+
// (sonar-go counts `&&`/`||` only); Python uses a distinct `boolean_operator`
|
|
12
|
+
// node and does not pass this predicate.
|
|
13
|
+
const C_BOOLEAN_OPS = new Set(['&&', '||', '??']);
|
|
14
|
+
// Returns the short-circuit logical operator KIND (`&&`/`||`/`??`) of a C-family
|
|
15
|
+
// `binary_expression` node, else null. The single source for "read a C-family
|
|
16
|
+
// boolean operator token": the cyclomatic predicate below counts any of the
|
|
17
|
+
// three; per-language cognitive `booleanOperatorKind` readers compare the kind
|
|
18
|
+
// for run-collapse (and filter out `??` where the language lacks it).
|
|
19
|
+
export function cFamilyBooleanOperatorKind(node) {
|
|
20
|
+
if (node.type !== 'binary_expression')
|
|
21
|
+
return null;
|
|
22
|
+
const op = node.childForFieldName('operator')?.type;
|
|
23
|
+
return op !== undefined && C_BOOLEAN_OPS.has(op) ? op : null;
|
|
24
|
+
}
|
|
25
|
+
export function isCFamilyBooleanOperator(node) {
|
|
26
|
+
return cFamilyBooleanOperatorKind(node) !== null;
|
|
27
|
+
}
|
|
28
|
+
// Only function/method symbols carry a cyclomatic number. The gate excludes the
|
|
29
|
+
// class-body PendingBody that TS/Python push for call resolution (its symbolId
|
|
30
|
+
// is the CLASS symbol — counting it would fold member control flow into a
|
|
31
|
+
// phantom). Bodiless symbols (interface methods, declarations) never reach here.
|
|
32
|
+
const COMPLEXITY_KINDS = new Set(['function', 'method']);
|
|
33
|
+
// A guard against generated/minified files: a parser table can have cyclomatic
|
|
34
|
+
// in the thousands and would otherwise dominate tool output and the (future)
|
|
35
|
+
// risk ranking on code agents never touch.
|
|
36
|
+
const COMPLEXITY_CAP = 999;
|
|
37
|
+
// The cognitive walk is RECURSIVE (the if/else-if chain, try/catch, and
|
|
38
|
+
// boolean-run flatten are irreducibly recursive — a frame stack would need
|
|
39
|
+
// synthetic marker frames and be more bug-prone). The Phase-2 stack-safety
|
|
40
|
+
// rationale (a generated file can be pathologically deep) is preserved by this
|
|
41
|
+
// explicit depth guard: descent stops past MAX_COGNITIVE_DEPTH. Real source
|
|
42
|
+
// nests orders of magnitude below this.
|
|
43
|
+
const MAX_COGNITIVE_DEPTH = 2000;
|
|
44
|
+
// Walks each function/method PendingBody and writes `symbol.complexity` onto the
|
|
45
|
+
// live Symbol instances (omitting the trivial value 1, the `receiver?`-omit
|
|
46
|
+
// hygiene). Mutates `symbols` in place; returns nothing. MUST run in the
|
|
47
|
+
// live-tree window (before pipeline.ts deletes the tree), at the per-language
|
|
48
|
+
// `resolveCalls` call site.
|
|
49
|
+
export function computeComplexity(bodies, symbols, opts) {
|
|
50
|
+
const byId = new Map(symbols.map((s) => [s.id, s]));
|
|
51
|
+
// Decision points accrue per symbolId so multiple bodies sharing an id
|
|
52
|
+
// accumulate (matters for Kotlin/C# later; a no-op for the MVP three, where
|
|
53
|
+
// each function/method has exactly one body). complexity = 1 + decisionPoints.
|
|
54
|
+
const decisionPoints = new Map();
|
|
55
|
+
// Cognitive points accrue the same way (sum across bodies, omit at 0). Null
|
|
56
|
+
// until a language opts in via `opts.cognitive`.
|
|
57
|
+
const cognitivePoints = opts.cognitive ? new Map() : null;
|
|
58
|
+
// symbolIds that have already taken the direct-recursion +1 (for `recursion.
|
|
59
|
+
// oncePerSymbol`). Owned here, across ALL of a symbol's bodies — a C# primary
|
|
60
|
+
// constructor pushes >1 body for one symbolId, so a per-body flag would re-count.
|
|
61
|
+
const recursedSymbols = new Set();
|
|
62
|
+
// The cyclomatic DFS may skip a wider set than the cognitive walk (Java
|
|
63
|
+
// excludes lambdas from cyclomatic but descends them for cognitive).
|
|
64
|
+
const cycSkip = opts.cyclomaticSkipTypes ?? opts.skipTypes;
|
|
65
|
+
for (const { symbolId, body } of bodies) {
|
|
66
|
+
const sym = byId.get(symbolId);
|
|
67
|
+
if (!sym || !COMPLEXITY_KINDS.has(sym.kind))
|
|
68
|
+
continue;
|
|
69
|
+
// If the body node is ITSELF a skip type — a curried/function-returning arrow
|
|
70
|
+
// whose `body` field is the inner `arrow_function` (`const g = (x) => (y) =>
|
|
71
|
+
// {…}`) — treat it as a separate scope and don't descend. The child loop
|
|
72
|
+
// below only skip-tests CHILDREN, so without this guard the root would bypass
|
|
73
|
+
// the skip check and leak the inner arrow's branches into this symbol.
|
|
74
|
+
if (opts.skipTypes.has(body.type))
|
|
75
|
+
continue;
|
|
76
|
+
let count = decisionPoints.get(symbolId) ?? 0;
|
|
77
|
+
// Iterative DFS (not recursion): `walkCalls` recurses unbounded, and a
|
|
78
|
+
// deeply-nested generated file could blow the stack. The skip test applies
|
|
79
|
+
// to CHILDREN (the root is handled by the guard above), mirroring walkCalls.
|
|
80
|
+
const stack = [body];
|
|
81
|
+
while (stack.length > 0) {
|
|
82
|
+
const node = stack.pop();
|
|
83
|
+
if (opts.decisionNodeTypes.has(node.type))
|
|
84
|
+
count++;
|
|
85
|
+
else if (opts.extraDecisionPredicate?.(node))
|
|
86
|
+
count++;
|
|
87
|
+
if (opts.cyclomaticDecrement?.(node))
|
|
88
|
+
count--; // SwiftLint fallthrough −1
|
|
89
|
+
for (const child of node.namedChildren) {
|
|
90
|
+
if (!cycSkip.has(child.type))
|
|
91
|
+
stack.push(child);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
decisionPoints.set(symbolId, count);
|
|
95
|
+
if (cognitivePoints) {
|
|
96
|
+
const prev = cognitivePoints.get(symbolId) ?? 0;
|
|
97
|
+
cognitivePoints.set(symbolId, prev + computeCognitive(body, opts.cognitive, opts.skipTypes, sym, recursedSymbols));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const [id, count] of decisionPoints) {
|
|
101
|
+
const dp = Math.max(0, count); // floor: a SwiftLint `fallthrough` −1 in a broken
|
|
102
|
+
if (dp === 0)
|
|
103
|
+
continue; // parse can't drive complexity below 1; trivial omitted
|
|
104
|
+
byId.get(id).complexity = Math.min(1 + dp, COMPLEXITY_CAP);
|
|
105
|
+
}
|
|
106
|
+
if (cognitivePoints) {
|
|
107
|
+
for (const [id, points] of cognitivePoints) {
|
|
108
|
+
if (points === 0)
|
|
109
|
+
continue; // cognitive 0 → omit (the receiver?-omit hygiene)
|
|
110
|
+
byId.get(id).cognitiveComplexity = Math.min(points, COMPLEXITY_CAP);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// True when `t` is one of the language's parenthesis-like wrappers. `spec` is a
|
|
115
|
+
// single `parenthesizedType` string for most languages, or a SET (C# — expression
|
|
116
|
+
// AND pattern parens) matched by membership.
|
|
117
|
+
function matchesParen(t, spec) {
|
|
118
|
+
return typeof spec === 'string' ? t === spec : spec.has(t);
|
|
119
|
+
}
|
|
120
|
+
// Unwraps nested parenthesized expressions to the inner expression (sonar's
|
|
121
|
+
// ExpressionUtils.skipParentheses), used while linearizing a boolean sequence.
|
|
122
|
+
function skipParens(node, parenSpec) {
|
|
123
|
+
let n = node;
|
|
124
|
+
while (n && matchesParen(n.type, parenSpec))
|
|
125
|
+
n = n.namedChild(0);
|
|
126
|
+
return n;
|
|
127
|
+
}
|
|
128
|
+
// First named child that is not a comment. tree-sitter attaches comments as
|
|
129
|
+
// NAMED children ("extras"), so positional access (namedChild(0)) can land on a
|
|
130
|
+
// comment instead of the real node — e.g. the body of `else /*c*/ {…}`. Every
|
|
131
|
+
// grammar wired so far names this node type `comment`.
|
|
132
|
+
function firstNonComment(node) {
|
|
133
|
+
let child = node.namedChild(0);
|
|
134
|
+
while (child && child.type === 'comment')
|
|
135
|
+
child = child.nextNamedSibling;
|
|
136
|
+
return child;
|
|
137
|
+
}
|
|
138
|
+
// Computes the cognitive complexity of ONE body subtree (whitepaper §1.2,
|
|
139
|
+
// clean-room verified against sonar-java's CognitiveComplexityVisitor). Returns
|
|
140
|
+
// the increment total (0 when trivial). Reuses the SAME skipTypes boundary as
|
|
141
|
+
// the cyclomatic walk so "this symbol's body" means the same thing for both
|
|
142
|
+
// metrics — methods containing anon/local classes therefore UNDER-COUNT vs
|
|
143
|
+
// sonar-java (which rolls those bodies into the enclosing method); a deliberate
|
|
144
|
+
// per-symbol-model divergence, like the TS/Py arrow-callback gap.
|
|
145
|
+
//
|
|
146
|
+
// Nesting starts at 0; a SURCHARGE node adds `1 + nesting` (the whitepaper
|
|
147
|
+
// "+1 plus one per level of nesting"; sonar-java's base-1 + `+= nesting` is
|
|
148
|
+
// algebraically identical). Booleans, labeled jumps, and `else`/`else if` are
|
|
149
|
+
// FLAT (+1, no surcharge). Lambdas raise nesting but add nothing.
|
|
150
|
+
function computeCognitive(body, cog, skipTypes, sym,
|
|
151
|
+
// symbolIds that already took the recursion +1 (for `recursion.oncePerSymbol`).
|
|
152
|
+
// Owned by computeComplexity and SHARED across all of a symbol's bodies, so a
|
|
153
|
+
// C# primary constructor (which pushes >1 body for one symbolId) counts recursion
|
|
154
|
+
// ONCE per method, not once per body.
|
|
155
|
+
recursedSymbols) {
|
|
156
|
+
let total = 0;
|
|
157
|
+
// Direct recursion (gocognit): +1 per bare self-call, but only for symbol
|
|
158
|
+
// kinds the language opts in (Go: 'function' — methods self-call via a
|
|
159
|
+
// selector, never a bare identifier, so they're excluded). Hoisted once; the
|
|
160
|
+
// `rec` capture lets TS narrow it inside visit() (no non-null assertions).
|
|
161
|
+
const rec = cog.recursion;
|
|
162
|
+
const recEligible = rec !== undefined && rec.eligibleKinds.has(sym.kind);
|
|
163
|
+
// node.id of every logical-operator node already counted as part of a boolean
|
|
164
|
+
// run, so the DFS doesn't recount them when it later descends the left spine.
|
|
165
|
+
// Keyed on node.id (stable across web-tree-sitter wrapper objects); MUST be
|
|
166
|
+
// call-local (per body) — hoisting it would undercount across symbols.
|
|
167
|
+
const counted = new Set();
|
|
168
|
+
// Per-language run-start rule; default = +1 at every operator-KIND change
|
|
169
|
+
// (sonar-java). TS overrides to count only `&&`-run-starts (SonarJS S3776).
|
|
170
|
+
const runStarts = cog.booleanRunStarts ?? ((kind, prev) => prev === null || prev !== kind);
|
|
171
|
+
// Logical-operand field names: default left/right; Swift's conjunction/disjunction
|
|
172
|
+
// nodes use lhs/rhs.
|
|
173
|
+
const leftField = cog.booleanLeftField ?? 'left';
|
|
174
|
+
const rightField = cog.booleanRightField ?? 'right';
|
|
175
|
+
// Parenthesis-like wrapper(s) to treat as transparent — a single type, or C#'s set
|
|
176
|
+
// of expression+pattern parens. Used by the source-order flatten AND the tree-scoped
|
|
177
|
+
// ancestor walk (both via `matchesParen`, which handles string or set).
|
|
178
|
+
const parenSpec = cog.parenthesizedType;
|
|
179
|
+
// Additional if-like node type(s) (Dart's collection `if_element`; Ruby's
|
|
180
|
+
// `unless`/`elsif`) are treated as `ifType`. `collectionIfType` is a single type
|
|
181
|
+
// (Dart) or a set (Ruby) — matched by equality or membership.
|
|
182
|
+
const isIfLike = (t) => t === cog.ifType ||
|
|
183
|
+
(cog.collectionIfType !== undefined &&
|
|
184
|
+
(typeof cog.collectionIfType === 'string'
|
|
185
|
+
? t === cog.collectionIfType
|
|
186
|
+
: cog.collectionIfType.has(t)));
|
|
187
|
+
const visitField = (node, field, nesting, depth) => {
|
|
188
|
+
const child = node.childForFieldName(field);
|
|
189
|
+
if (child)
|
|
190
|
+
visit(child, nesting, depth + 1);
|
|
191
|
+
};
|
|
192
|
+
// Walks an `if`'s condition at base nesting. Field-based by default (`initField`
|
|
193
|
+
// + `conditionField`); when `conditionFromNamedChildren` is set (Dart's fieldless
|
|
194
|
+
// condition/pattern/`when`-guard), walks every named child that is NOT the
|
|
195
|
+
// consequence/alternative field-child (and not a comment) so the condition's
|
|
196
|
+
// booleans and Dart-3 pattern guards count. Shared by the head-if and the
|
|
197
|
+
// else-if branch so the two never drift.
|
|
198
|
+
const visitIfCondition = (ifNode, nesting, depth) => {
|
|
199
|
+
if (cog.conditionFromNamedChildren) {
|
|
200
|
+
const cons = ifNode.childForFieldName(cog.consequenceField);
|
|
201
|
+
const alt = ifNode.childForFieldName(cog.alternativeField);
|
|
202
|
+
for (const c of ifNode.namedChildren) {
|
|
203
|
+
if (c.type === 'comment')
|
|
204
|
+
continue;
|
|
205
|
+
if ((cons && c.id === cons.id) || (alt && c.id === alt.id))
|
|
206
|
+
continue;
|
|
207
|
+
visit(c, nesting, depth + 1);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (cog.initField)
|
|
212
|
+
visitField(ifNode, cog.initField, nesting, depth);
|
|
213
|
+
visitField(ifNode, cog.conditionField, nesting, depth);
|
|
214
|
+
};
|
|
215
|
+
// Python's `else_clause` (+1 FLAT, body one level deeper). Shared by BOTH the
|
|
216
|
+
// handleAlternative path (an `if`'s own terminal else, consumed there so it isn't
|
|
217
|
+
// re-dispatched) and the visit() dispatch (a for/while/try `else` reached via
|
|
218
|
+
// general descent) — one helper so the two mutually-exclusive paths can't drift.
|
|
219
|
+
const visitElseClauseBody = (clause, nesting, depth) => {
|
|
220
|
+
total += 1;
|
|
221
|
+
for (const child of clause.namedChildren)
|
|
222
|
+
visit(child, nesting + 1, depth + 1);
|
|
223
|
+
};
|
|
224
|
+
// Walks an `if`'s else/else-if chain. The head `if` is handled by the caller
|
|
225
|
+
// (it surcharges); each link here is +1 FLAT. An `else if` (alternative is
|
|
226
|
+
// another `if`) keeps the chain's base nesting for its own condition and
|
|
227
|
+
// surcharge-free body; a plain `else` scans its body one level deeper.
|
|
228
|
+
const handleAlternative = (ifNode, nesting, depth) => {
|
|
229
|
+
// Bound the else-if chain recursion by the same depth guard as `visit` — a
|
|
230
|
+
// pathologically long `if/else if/else if/…` chain would otherwise blow the
|
|
231
|
+
// native stack despite MAX_COGNITIVE_DEPTH (the chain recurses here, not
|
|
232
|
+
// through `visit`).
|
|
233
|
+
if (depth > MAX_COGNITIVE_DEPTH)
|
|
234
|
+
return;
|
|
235
|
+
// Python: the `alternative` field is a flat LIST of elif_clause / else_clause
|
|
236
|
+
// SIBLINGS (not a nested-if chain). Each elif/else is +1 FLAT; the elif
|
|
237
|
+
// condition stays at base nesting (its booleans are flat anyway), every clause
|
|
238
|
+
// BODY is one level deeper. No recursion — the clauses are siblings, not nested.
|
|
239
|
+
if (cog.elifClauseType) {
|
|
240
|
+
for (const alt of ifNode.childrenForFieldName(cog.alternativeField)) {
|
|
241
|
+
if (alt.type === cog.elifClauseType) {
|
|
242
|
+
total += 1;
|
|
243
|
+
visitField(alt, cog.conditionField, nesting, depth);
|
|
244
|
+
visitField(alt, cog.consequenceField, nesting + 1, depth);
|
|
245
|
+
}
|
|
246
|
+
else if (cog.elseClauseType && alt.type === cog.elseClauseType) {
|
|
247
|
+
// PHP two-word `else if`: an `else` clause whose body is itself an `if`.
|
|
248
|
+
// SonarPHP gives the inner if +1 FLAT (no `else` +1) but inside the else
|
|
249
|
+
// clause's own nesting bump, so its body is one level deeper than a one-word
|
|
250
|
+
// elseif (elseChainsIf). Otherwise a plain `else`: +1 flat, body nested.
|
|
251
|
+
const innerIf = cog.elseChainsIf ? alt.childForFieldName(cog.consequenceField) : null;
|
|
252
|
+
if (innerIf && innerIf.type === cog.ifType) {
|
|
253
|
+
const n2 = nesting + 1; // the else clause's nesting bump wraps the inner if
|
|
254
|
+
total += 1; // inner if: +1 FLAT (SonarPHP's ifStatementWithoutNesting)
|
|
255
|
+
visitIfCondition(innerIf, n2, depth);
|
|
256
|
+
visitField(innerIf, cog.consequenceField, n2 + 1, depth);
|
|
257
|
+
handleAlternative(innerIf, n2, depth + 1);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
visitElseClauseBody(alt, nesting, depth);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const rawAlt = ifNode.childForFieldName(cog.alternativeField);
|
|
267
|
+
if (!rawAlt)
|
|
268
|
+
return;
|
|
269
|
+
// Unwrap an else-wrapper node (TS `else_clause`) to the real else-if / else
|
|
270
|
+
// body; grammars that hold the if/block directly (Java) leave elseClauseType
|
|
271
|
+
// unset and use rawAlt as-is. SKIP a leading comment: tree-sitter attaches a
|
|
272
|
+
// comment sitting between `else` and the body as a NAMED child of the wrapper
|
|
273
|
+
// (`else /*c*/ {…}`), so a bare namedChild(0) would grab the comment and drop
|
|
274
|
+
// the entire else body's complexity.
|
|
275
|
+
const alt = cog.elseClauseType && rawAlt.type === cog.elseClauseType
|
|
276
|
+
? firstNonComment(rawAlt)
|
|
277
|
+
: rawAlt;
|
|
278
|
+
if (!alt)
|
|
279
|
+
return;
|
|
280
|
+
if (isIfLike(alt.type)) {
|
|
281
|
+
total += 1; // the `else if` keyword: +1 flat, no surcharge (elsif chains always count)
|
|
282
|
+
visitIfCondition(alt, nesting, depth);
|
|
283
|
+
visitField(alt, cog.consequenceField, nesting + 1, depth);
|
|
284
|
+
handleAlternative(alt, nesting, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Plain (terminal) else: +1 flat for the `else` keyword — UNLESS the head if is
|
|
288
|
+
// an EXPRESSION-TERNARY (Ruby: a simple 2-branch if-else used as an expression,
|
|
289
|
+
// where SLANG's isTernaryOperator suppresses the else). Unset elsewhere → always
|
|
290
|
+
// +1 (sonar-java/whitepaper). Body at nesting+1 (sonar default) or base nesting
|
|
291
|
+
// (gocognit, via nestElseBody === false; `=== false` keeps unset = sonar-java).
|
|
292
|
+
if (!cog.isExpressionTernary || !cog.isExpressionTernary(ifNode))
|
|
293
|
+
total += 1;
|
|
294
|
+
visit(alt, cog.nestElseBody === false ? nesting : nesting + 1, depth + 1);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// Swift's POSITIONAL `if` (cog.ifPositionalBlockType set): no consequence/
|
|
298
|
+
// alternative field. Conditions are `conditionField` children (possibly several:
|
|
299
|
+
// `if a, let b = c`); the consequence is the block child BEFORE the `else` keyword;
|
|
300
|
+
// the else branch (an `ifType` else-if, or a plain-else block AFTER `else`) is split
|
|
301
|
+
// off at the `else` keyword (elseKeywordType) — NOT inferred from a second block
|
|
302
|
+
// child, because an empty `{}` body emits NO block node, which would otherwise drop
|
|
303
|
+
// the `+1 FLAT` for an empty-consequence or empty-else branch. The head if's surcharge
|
|
304
|
+
// is added by the caller; this walks the conditions (base nesting, booleans flat), the
|
|
305
|
+
// consequence (nesting+1), and the FLAT else/else-if chain (each +1, no surcharge —
|
|
306
|
+
// recurses for else-if WITHOUT a head surcharge). Depth-guarded like handleAlternative
|
|
307
|
+
// (a long else-if chain recurses here, not through `visit`).
|
|
308
|
+
const visitPositionalIfBody = (ifNode, nesting, depth) => {
|
|
309
|
+
if (depth > MAX_COGNITIVE_DEPTH)
|
|
310
|
+
return;
|
|
311
|
+
// Conditions: base nesting, booleans flat (Swift allows several — `if a, let b = c`).
|
|
312
|
+
// condIds is built ONLY for the Kotlin path (it excludes the condition field-children —
|
|
313
|
+
// a field child is also a named child — when locating the consequence/else among
|
|
314
|
+
// namedChildren); Swift finds the consequence by block type and never reads it, so it
|
|
315
|
+
// stays null (no allocation on the Swift path).
|
|
316
|
+
const condIds = cog.ifConsequenceFromNamedChildren ? new Set() : null;
|
|
317
|
+
for (const cond of ifNode.childrenForFieldName(cog.conditionField)) {
|
|
318
|
+
condIds?.add(cond.id);
|
|
319
|
+
visit(cond, nesting, depth + 1);
|
|
320
|
+
}
|
|
321
|
+
// Kotlin: the `else` keyword is ANONYMOUS and a branch may be BRACE-LESS, so SPLIT at
|
|
322
|
+
// the `else` keyword (always emitted for a real else, even when a branch is a `;` empty
|
|
323
|
+
// statement that emits no named child — the empty-branch lesson). The consequence is the
|
|
324
|
+
// first named non-condition non-comment child BEFORE `else`; the else body is the first
|
|
325
|
+
// such child AFTER it (each may be absent for a `;` branch).
|
|
326
|
+
if (cog.ifConsequenceFromNamedChildren) {
|
|
327
|
+
const isBody = (c) => c.isNamed && !condIds.has(c.id) && c.type !== 'line_comment' && c.type !== 'block_comment';
|
|
328
|
+
const kids = ifNode.children;
|
|
329
|
+
const elseIdx = cog.elseKeywordType ? kids.findIndex((c) => c.type === cog.elseKeywordType) : -1;
|
|
330
|
+
const consequence = (elseIdx === -1 ? kids : kids.slice(0, elseIdx)).find(isBody);
|
|
331
|
+
if (consequence)
|
|
332
|
+
visit(consequence, nesting + 1, depth + 1);
|
|
333
|
+
if (elseIdx === -1)
|
|
334
|
+
return; // no else clause
|
|
335
|
+
const elseBody = kids.slice(elseIdx + 1).find(isBody);
|
|
336
|
+
// sonar-kotlin charges the else +1 ONLY when the else body is a block (`{}`, the
|
|
337
|
+
// elseChargeBlockType) or an else-if (`ifType`) — a BRACE-LESS `else expr` is the
|
|
338
|
+
// ternary form and gets NO +1. The else body still NESTS regardless.
|
|
339
|
+
const isElseIf = elseBody?.type === cog.ifType;
|
|
340
|
+
if (isElseIf || elseBody?.type === cog.elseChargeBlockType)
|
|
341
|
+
total += 1; // +1 FLAT, no surcharge
|
|
342
|
+
if (isElseIf)
|
|
343
|
+
visitPositionalIfBody(elseBody, nesting, depth + 1); // else-if chain, no head surcharge
|
|
344
|
+
else if (elseBody)
|
|
345
|
+
visit(elseBody, nesting + 1, depth + 1); // plain else body nests
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const blockType = cog.ifPositionalBlockType;
|
|
349
|
+
const kids = ifNode.namedChildren;
|
|
350
|
+
// The `else` keyword (a named token) signals the else clause and splits the body
|
|
351
|
+
// from it — even when either block is the empty `{}` that emits no block node.
|
|
352
|
+
const elseIdx = cog.elseKeywordType ? kids.findIndex((c) => c.type === cog.elseKeywordType) : -1;
|
|
353
|
+
const beforeElse = elseIdx === -1 ? kids : kids.slice(0, elseIdx);
|
|
354
|
+
const consequence = beforeElse.find((c) => c.type === blockType);
|
|
355
|
+
if (consequence)
|
|
356
|
+
visit(consequence, nesting + 1, depth + 1);
|
|
357
|
+
if (elseIdx === -1)
|
|
358
|
+
return; // no else clause
|
|
359
|
+
total += 1; // else / else-if: +1 FLAT, no surcharge
|
|
360
|
+
const afterElse = kids.slice(elseIdx + 1);
|
|
361
|
+
const elseIf = afterElse.find((c) => c.type === cog.ifType);
|
|
362
|
+
if (elseIf) {
|
|
363
|
+
visitPositionalIfBody(elseIf, nesting, depth + 1); // recurse the chain
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const elseBody = afterElse.find((c) => c.type === blockType);
|
|
367
|
+
if (elseBody)
|
|
368
|
+
visit(elseBody, nesting + 1, depth + 1); // may be absent (empty else)
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
function visit(node, nesting, depth) {
|
|
372
|
+
if (depth > MAX_COGNITIVE_DEPTH)
|
|
373
|
+
return;
|
|
374
|
+
const t = node.type;
|
|
375
|
+
if (skipTypes.has(t))
|
|
376
|
+
return; // nested classes / methods: own symbols' bodies
|
|
377
|
+
// --- direct recursion: +1 FLAT per self-call site (no surcharge, no return:
|
|
378
|
+
// the call's arguments still descend below to catch nested control flow). A
|
|
379
|
+
// call node is never a boolean (so never in `counted`) and matches no other
|
|
380
|
+
// dispatch branch, so this can't double-count. The walk descends func_literal
|
|
381
|
+
// (nestOnly), so a self-call inside a closure counts toward the enclosing
|
|
382
|
+
// function — matching gocognit, whose FuncDecl-rooted walk doesn't reset the
|
|
383
|
+
// target across closures. ---
|
|
384
|
+
if (recEligible && rec && t === rec.callType) {
|
|
385
|
+
// null/undefined callee never equals the (always-non-empty) symbol name.
|
|
386
|
+
const isSelf = rec.isSelfCall
|
|
387
|
+
? rec.isSelfCall(node, body, sym)
|
|
388
|
+
: rec.bareCalleeName?.(node) === sym.name;
|
|
389
|
+
if (isSelf) {
|
|
390
|
+
// oncePerSymbol (C#): +1 only the FIRST time across the symbol's bodies.
|
|
391
|
+
// Per-site (Go, oncePerSymbol unset): +1 every time, `recursedSymbols` untouched.
|
|
392
|
+
if (!rec.oncePerSymbol || !recursedSymbols.has(sym.id))
|
|
393
|
+
total += 1;
|
|
394
|
+
if (rec.oncePerSymbol)
|
|
395
|
+
recursedSymbols.add(sym.id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// --- if / else-if / else chain (head if surcharges; chain links are flat).
|
|
399
|
+
// `isIfLike` also matches Dart's collection `if_element` (an if inside a
|
|
400
|
+
// collection literal — same surcharge/else handling). ---
|
|
401
|
+
if (isIfLike(t)) {
|
|
402
|
+
total += 1 + nesting;
|
|
403
|
+
if (cog.ifPositionalBlockType || cog.ifConsequenceFromNamedChildren) {
|
|
404
|
+
// Positional consequence/else (no fields): Swift (keyword-split) or Kotlin
|
|
405
|
+
// (anon `else` + brace-less branches). See visitPositionalIfBody.
|
|
406
|
+
visitPositionalIfBody(node, nesting, depth);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
visitIfCondition(node, nesting, depth);
|
|
410
|
+
visitField(node, cog.consequenceField, nesting + 1, depth);
|
|
411
|
+
handleAlternative(node, nesting, depth);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// --- for/while/try `else` clause (Python): +1 flat, body one level deeper.
|
|
416
|
+
// Gated on elifClauseType (Python mode). The `if`'s OWN else is consumed by
|
|
417
|
+
// handleAlternative (the if-case returns), so this fires only for a loop/try
|
|
418
|
+
// `else_clause` reached via general descent — matching sonar-python's
|
|
419
|
+
// `visitElseClause` (+1 flat) with the else body StatementList nesting one level. ---
|
|
420
|
+
if (cog.elifClauseType && cog.elseClauseType && t === cog.elseClauseType) {
|
|
421
|
+
visitElseClauseBody(node, nesting, depth);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// --- loops: surcharge, then nest the body. With loopBodyField set (Python),
|
|
425
|
+
// ONLY the body child nests and the iterable/condition/target/`else` stay at the
|
|
426
|
+
// loop's ambient nesting (sonar-python). Without it, ALL children nest — the
|
|
427
|
+
// KNOWN DIVERGENCE (Java/TS/Go, accepted): sonar nests ONLY the body, not the
|
|
428
|
+
// loop header, so a nested STRUCTURAL construct in the header (`for(;cond?a:b;)`)
|
|
429
|
+
// over-counts; booleans are flat (unaffected) and headers rarely hold control
|
|
430
|
+
// flow (0 cases in the TS/gson/gocognit oracles). The precise per-construct-body
|
|
431
|
+
// fix landed for Python (loopBodyField); generalizing it needs re-oracling the
|
|
432
|
+
// other grammars, deferred. ---
|
|
433
|
+
if (cog.loopTypes.has(t)) {
|
|
434
|
+
total += 1 + nesting;
|
|
435
|
+
const bodyChild = cog.loopBodyField ? node.childForFieldName(cog.loopBodyField) : null;
|
|
436
|
+
for (const child of node.namedChildren) {
|
|
437
|
+
const inner = cog.loopBodyField
|
|
438
|
+
? bodyChild && child.id === bodyChild.id
|
|
439
|
+
? nesting + 1
|
|
440
|
+
: nesting
|
|
441
|
+
: nesting + 1;
|
|
442
|
+
visit(child, inner, depth + 1);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// --- surcharge-FLAT (C# `goto`/`goto case`): surcharge (+1 + nesting) but
|
|
447
|
+
// descend children at the SAME nesting — the construct itself is a flow-breaker,
|
|
448
|
+
// but a structural construct in its operand (`goto case (p ? 1 : 2)`) is NOT
|
|
449
|
+
// nested by SonarC# (it's counted at the goto's own nesting). Kept separate from
|
|
450
|
+
// the switch/ternary branch, which DOES nest its children. ---
|
|
451
|
+
if (cog.surchargeTypes?.has(t)) {
|
|
452
|
+
total += 1 + nesting;
|
|
453
|
+
for (const child of node.namedChildren)
|
|
454
|
+
visit(child, nesting, depth + 1);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// --- ternary with BRANCH-ONLY nesting (PHP): surcharge, but nest ONLY the
|
|
458
|
+
// configured branch fields (true/false); the CONDITION stays at the ternary's
|
|
459
|
+
// ambient nesting. Resolves the overbump for a chained elvis `a ?: b ?: c`
|
|
460
|
+
// (`((a ?: b) ?: c)`), whose inner conditionals sit in the CONDITION position —
|
|
461
|
+
// bump-all would compound the surcharge per link (SonarPHP visits the condition at
|
|
462
|
+
// ambient). The loopBodyField analog for ternaries; only PHP sets ternaryBranchFields. ---
|
|
463
|
+
if (t === cog.ternaryType && cog.ternaryBranchFields) {
|
|
464
|
+
total += 1 + nesting;
|
|
465
|
+
const branchIds = new Set();
|
|
466
|
+
for (const f of cog.ternaryBranchFields) {
|
|
467
|
+
const c = node.childForFieldName(f);
|
|
468
|
+
if (c)
|
|
469
|
+
branchIds.add(c.id);
|
|
470
|
+
}
|
|
471
|
+
for (const child of node.namedChildren) {
|
|
472
|
+
visit(child, branchIds.has(child.id) ? nesting + 1 : nesting, depth + 1);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// --- switch / ternary: surcharge, then ALL children one level deeper. (sonar
|
|
477
|
+
// nests the WHOLE ternary subtree — incl. its test — so bump-all is correct for
|
|
478
|
+
// a ternary; the header-overbump caveat above applies to switch discriminants.)
|
|
479
|
+
// A statement-position ternary (Ruby: parent ∈ ternaryStatementParentTypes) is
|
|
480
|
+
// an if-else, so it also takes the `else` keyword's +1 FLAT. ---
|
|
481
|
+
if (cog.switchTypes.has(t) || t === cog.ternaryType) {
|
|
482
|
+
total += 1 + nesting;
|
|
483
|
+
// A STATEMENT-position ternary (Ruby) is an if-else → also +1 for the `else`;
|
|
484
|
+
// an EXPRESSION-position ternary is the surcharge-only form. Unset elsewhere
|
|
485
|
+
// (other languages' ternaries are always surcharge-only).
|
|
486
|
+
if (t === cog.ternaryType && cog.isExpressionTernary && !cog.isExpressionTernary(node)) {
|
|
487
|
+
total += 1;
|
|
488
|
+
}
|
|
489
|
+
for (const child of node.namedChildren)
|
|
490
|
+
visit(child, nesting + 1, depth + 1);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// --- catch clause: surcharge at the current (try's) nesting, body one level
|
|
494
|
+
// deeper. Handled as its own case rather than nested inside a try-node
|
|
495
|
+
// branch, so it fires for ANY try container — `try_statement` AND
|
|
496
|
+
// `try_with_resources_statement` — which are themselves plain pass-through
|
|
497
|
+
// (the try body / resource spec / `finally` add nothing and don't bump). ---
|
|
498
|
+
if (t === cog.catchType && (!cog.catchPredicate || cog.catchPredicate(node))) {
|
|
499
|
+
total += 1 + nesting;
|
|
500
|
+
for (const child of node.namedChildren)
|
|
501
|
+
visit(child, nesting + 1, depth + 1);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// --- try container with FLAT catch bodies (Dart): the try body (`bodyField`
|
|
505
|
+
// child) and the `on`/type/`catch_clause` headers + `finally` stay at base
|
|
506
|
+
// nesting; EACH other direct `catchBodyType` child is a catch body that
|
|
507
|
+
// surcharges (+1 + nesting) and nests at +1. This covers `catch(e){}`,
|
|
508
|
+
// `on E catch(e){}`, AND a binding-less `on E {}` (a block with no catch_clause),
|
|
509
|
+
// all of which the SonarQube Dart rule counts as a catch. Other languages leave tryType unset
|
|
510
|
+
// (their catch clause contains its body — handled by the catchType branch). ---
|
|
511
|
+
if (cog.tryType && t === cog.tryType.node) {
|
|
512
|
+
const tryBody = node.childForFieldName(cog.tryType.bodyField);
|
|
513
|
+
for (const child of node.namedChildren) {
|
|
514
|
+
// A catch body = a `catchBodyType` child that is NOT the try body. The
|
|
515
|
+
// `tryBody &&` guard matters on a malformed parse where the `body:` field
|
|
516
|
+
// didn't bind (tryBody null): without it the try body itself would be
|
|
517
|
+
// mis-surcharged as a catch (an over-count); instead descend everything at
|
|
518
|
+
// base nesting (a safe under-count). Valid Dart always binds `body:`.
|
|
519
|
+
if (tryBody && child.type === cog.tryType.catchBodyType && child.id !== tryBody.id) {
|
|
520
|
+
total += 1 + nesting;
|
|
521
|
+
visit(child, nesting + 1, depth + 1);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
visit(child, nesting, depth + 1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// --- nesting-only (lambda): raise nesting, add nothing ---
|
|
530
|
+
if (cog.nestOnlyTypes.has(t)) {
|
|
531
|
+
for (const child of node.namedChildren)
|
|
532
|
+
visit(child, nesting + 1, depth + 1);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// --- labeled break/continue: +1 flat if it jumps to a label, then DESCEND. In
|
|
536
|
+
// Rust `break`/`continue` are EXPRESSIONS that can carry a value (`break a && b`,
|
|
537
|
+
// `break if c {1} else {2}`) whose control flow counts (flat, no extra bump); the
|
|
538
|
+
// label is a `label` leaf that matches no branch, so descending it is a no-op.
|
|
539
|
+
// Go/Java/TS/Python jumps hold only a label/nothing, so the descent is a no-op
|
|
540
|
+
// there too (verified: the full suite is unchanged). ---
|
|
541
|
+
if (cog.labeledJumpTypes.has(t)) {
|
|
542
|
+
if (cog.hasLabel(node))
|
|
543
|
+
total += 1;
|
|
544
|
+
for (const child of node.namedChildren)
|
|
545
|
+
visit(child, nesting, depth + 1);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// --- boolean runs (TREE-SCOPED, the SonarQube Dart model): a logical node adds +1 iff its
|
|
549
|
+
// operator kind differs from its nearest LOGICAL ANCESTOR (skipping parens) —
|
|
550
|
+
// a top-of-tree logical node or a kind-change starts a run. No flatten: each
|
|
551
|
+
// logical node is processed once as the DFS reaches it, operands descend below. ---
|
|
552
|
+
if (cog.booleanByTreeParent) {
|
|
553
|
+
const kind = cog.booleanOperatorKind(node);
|
|
554
|
+
if (kind !== null) {
|
|
555
|
+
let anc = node.parent;
|
|
556
|
+
while (anc && matchesParen(anc.type, parenSpec))
|
|
557
|
+
anc = anc.parent;
|
|
558
|
+
const ancKind = anc ? cog.booleanOperatorKind(anc) : null;
|
|
559
|
+
if (ancKind !== kind)
|
|
560
|
+
total += 1; // new run (different or no logical parent)
|
|
561
|
+
for (const child of node.namedChildren)
|
|
562
|
+
visit(child, nesting, depth + 1);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// --- boolean runs: +1 per maximal same-kind sequence in SOURCE order ---
|
|
567
|
+
if (cog.booleanOperatorKind(node) !== null) {
|
|
568
|
+
if (!counted.has(node.id)) {
|
|
569
|
+
// Linearize the whole boolean tree IN SOURCE ORDER (sonar's
|
|
570
|
+
// flattenLogicalExpression): in-order over BOTH operands, unwrapping
|
|
571
|
+
// parens, so `a && b && (c||d)` → [&&, &&, ||]. A +1 is charged at the
|
|
572
|
+
// start and at each operator-kind change. (A left-spine-only flatten
|
|
573
|
+
// would wrongly merge &&s split by a parenthesized || — oracle-caught.)
|
|
574
|
+
const run = [];
|
|
575
|
+
// `d` carries the visit depth so a pathologically long boolean spine
|
|
576
|
+
// (`a && a && … `, tens of thousands of operands in generated code)
|
|
577
|
+
// can't overflow the native stack — bounded by the same guard as `visit`.
|
|
578
|
+
const flatten = (n, d) => {
|
|
579
|
+
if (d > MAX_COGNITIVE_DEPTH)
|
|
580
|
+
return;
|
|
581
|
+
const inner = skipParens(n, parenSpec);
|
|
582
|
+
if (inner && cog.booleanOperatorKind(inner) !== null) {
|
|
583
|
+
counted.add(inner.id);
|
|
584
|
+
// Operands via the operand FIELDS (default `left`/`right`, Swift
|
|
585
|
+
// `lhs`/`rhs` via booleanLeftField/booleanRightField), not positional
|
|
586
|
+
// namedChild(0)/(1): a comment interleaved around the operator
|
|
587
|
+
// (`a && /*c*/ b`) is a named child, so positional access would read
|
|
588
|
+
// the comment as the right operand and drop a parenthesized sub-run.
|
|
589
|
+
// Every logical node so far (Java/TS/Go/Rust `binary_expression`,
|
|
590
|
+
// Python `boolean_operator`, Swift `conjunction_expression`/
|
|
591
|
+
// `disjunction_expression`) exposes a left/right operand field pair;
|
|
592
|
+
flatten(inner.childForFieldName(leftField), d + 1);
|
|
593
|
+
run.push(inner);
|
|
594
|
+
flatten(inner.childForFieldName(rightField), d + 1);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
flatten(node, depth); // marks the whole subtree `counted`
|
|
598
|
+
// A run whose ROOT is excluded (TS JSX short-circuit) contributes 0 —
|
|
599
|
+
// but the flatten above still ran, so its inner logical nodes won't be
|
|
600
|
+
// recounted when the DFS descends below.
|
|
601
|
+
if (!cog.excludeBooleanRun?.(node)) {
|
|
602
|
+
let prevKind = null;
|
|
603
|
+
for (const n of run) {
|
|
604
|
+
const kind = cog.booleanOperatorKind(n); // non-null: it's in `run`
|
|
605
|
+
if (runStarts(kind, prevKind))
|
|
606
|
+
total += 1;
|
|
607
|
+
prevKind = kind;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Descend operands at the SAME nesting (booleans are flat). The flattened
|
|
612
|
+
// logical nodes are in `counted` so they skip the run-count but are still
|
|
613
|
+
// descended (to catch a nested ternary / control structure in an operand).
|
|
614
|
+
for (const child of node.namedChildren)
|
|
615
|
+
visit(child, nesting, depth + 1);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// --- flat conditional (Rust `let … else`): +1 flat, then descend at the
|
|
619
|
+
// SAME nesting (no surcharge, no bump — the value expr and the divergent else
|
|
620
|
+
// block both stay at the binding's level). ---
|
|
621
|
+
if (cog.flatIncrement?.(node)) {
|
|
622
|
+
total += 1;
|
|
623
|
+
for (const child of node.namedChildren)
|
|
624
|
+
visit(child, nesting, depth + 1);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// --- default: pass through, nesting unchanged ---
|
|
628
|
+
for (const child of node.namedChildren)
|
|
629
|
+
visit(child, nesting, depth + 1);
|
|
630
|
+
}
|
|
631
|
+
visit(body, 0, 0);
|
|
632
|
+
return total;
|
|
633
|
+
}
|