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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/config.js +223 -0
  4. package/dist/git/analyzer.js +177 -0
  5. package/dist/git/git-service.js +568 -0
  6. package/dist/git/head-watcher.js +113 -0
  7. package/dist/git/runner.js +204 -0
  8. package/dist/index.js +138 -0
  9. package/dist/indexer/code-index.js +1801 -0
  10. package/dist/indexer/complexity.js +633 -0
  11. package/dist/indexer/extractor.js +354 -0
  12. package/dist/indexer/languages/cpp.js +934 -0
  13. package/dist/indexer/languages/csharp.js +854 -0
  14. package/dist/indexer/languages/dart.js +777 -0
  15. package/dist/indexer/languages/go.js +665 -0
  16. package/dist/indexer/languages/java.js +507 -0
  17. package/dist/indexer/languages/kotlin.js +709 -0
  18. package/dist/indexer/languages/objc.js +397 -0
  19. package/dist/indexer/languages/php.js +771 -0
  20. package/dist/indexer/languages/python.js +455 -0
  21. package/dist/indexer/languages/ruby.js +697 -0
  22. package/dist/indexer/languages/rust.js +754 -0
  23. package/dist/indexer/languages/swift.js +691 -0
  24. package/dist/indexer/languages/typescript.js +485 -0
  25. package/dist/indexer/parser.js +175 -0
  26. package/dist/indexer/pipeline.js +342 -0
  27. package/dist/indexer/scanner.js +279 -0
  28. package/dist/indexer/watcher.js +353 -0
  29. package/dist/logger.js +16 -0
  30. package/dist/server.js +170 -0
  31. package/dist/tools/common.js +207 -0
  32. package/dist/tools/find-references.js +224 -0
  33. package/dist/tools/find-symbol.js +94 -0
  34. package/dist/tools/get-context.js +370 -0
  35. package/dist/tools/impact.js +218 -0
  36. package/dist/tools/overview.js +482 -0
  37. package/dist/tools/search-structure.js +303 -0
  38. package/dist/types.js +61 -0
  39. package/grammars/tree-sitter-c.wasm +0 -0
  40. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  41. package/grammars/tree-sitter-cpp.wasm +0 -0
  42. package/grammars/tree-sitter-dart.wasm +0 -0
  43. package/grammars/tree-sitter-go.wasm +0 -0
  44. package/grammars/tree-sitter-java.wasm +0 -0
  45. package/grammars/tree-sitter-javascript.wasm +0 -0
  46. package/grammars/tree-sitter-kotlin.wasm +0 -0
  47. package/grammars/tree-sitter-objc.wasm +0 -0
  48. package/grammars/tree-sitter-php.wasm +0 -0
  49. package/grammars/tree-sitter-python.wasm +0 -0
  50. package/grammars/tree-sitter-ruby.wasm +0 -0
  51. package/grammars/tree-sitter-rust.wasm +0 -0
  52. package/grammars/tree-sitter-swift.wasm +0 -0
  53. package/grammars/tree-sitter-tsx.wasm +0 -0
  54. package/grammars/tree-sitter-typescript.wasm +0 -0
  55. 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
+ }