fix(complexity): add cognitive/cyclomatic/Halstead support for c/cpp/kotlin/swift/scala/bash#2059
Conversation
…kotlin/swift/scala/bash Ports the tier-1 languages from issue #1923 (native rules already existed, WASM/TS had no COMPLEXITY_RULES/HALSTEAD_RULES entries at all) into src/ast-analysis/rules/{c,b2,b3}.ts and wires them into COMPLEXITY_RULES/ HALSTEAD_RULES. Verified every rule against real tree-sitter output rather than assuming the native config was correct, which surfaced and fixed three real bugs affecting both engines: - C/CPP_RULES had else_via_alternative backwards: tree-sitter-c/cpp wrap the else branch in a real else_clause node (Pattern A, like JS/C#/Rust), not Go/Java's alternative-field pattern. A canonical if/else-if/else computed cognitive=5, maxNesting=2 instead of the correct cognitive=3, maxNesting=1. - BASH_RULES was missing else_clause from branch_nodes, so a trailing plain else got zero cognitive credit despite else_node_type being configured. - SWIFT_RULES used logical_node_types: ["binary_expression"], but Swift (like Kotlin) splits && / || into conjunction_expression/disjunction_expression — so Swift's && / || were never counted at all. Also fixed a WASM-only divergence from the Rust engine: computeFunctionComplexity and the complexity visitor evaluated branchNodes and caseNodes independently, double-counting any node type listed in both sets (Kotlin's when_entry, Scala's case_clause). Rust's walk() returns unconditionally after a branch-node match, making case handling unreachable for such types; TS now mirrors that exclusivity. Widened ComplexityRules.logicalNodeType (singular) to logicalNodeTypes (Set), matching the native logical_node_types slice, so Kotlin/Swift's split && / || node types can both be registered — the old singular field could only ever hold one type. Filed #2058 for the remaining known-imperfect native rules mirrored as-is for engine parity: case-node shadowing for C/C++/Kotlin/Scala's switch/match (inflates cognitive complexity per case), Swift's switch container missing entirely from branch_nodes, Kotlin/Swift's else-if chains counting as progressively nested branches instead of a flat chain (needs a new sibling-position detection primitive, not a config fix), and c/cpp/kotlin/ swift/scala's comment-prefix list undercounting multi-line block comments. The remaining 17 tier-2 languages (functional/pattern-matching languages needing from-scratch grammar design, per #1923's own scoping) are left for follow-up work under #1923, which stays open.
Codegraph Impact Analysis5 functions changed → 11 callers affected across 2 files
|
Greptile SummaryThis PR ports six tier-1 languages (C, C++, Kotlin, Swift, Scala, Bash) to the WASM complexity engine by adding
Confidence Score: 4/5Safe to merge — all three native bugs are verified against real tree-sitter output and the native Rust and WASM engines now produce byte-identical JSON for fixture files in all six languages. The core algorithm changes (isBranchNode guard, handleBranchNode always returning true) are correct and fully covered by new DFS-vs-visitor parity tests. One low-risk structural oddity remains: switch_statement appears in switchLikeNodes for C/C++ but not in branchNodes, making the switchLikeNodes entry unreachable at runtime — an exact mirror of the native config, intentionally deferred to #2058. No data loss or incorrect metric values introduced. src/ast-analysis/rules/c.ts — the switch_statement/switchLikeNodes dead-config detail is worth a second glance, though it is intentional parity with the native engine. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Node visited] --> B{in branchNodes\nAND childCount > 0?}
B -- No --> C{in caseNodes\nAND childCount > 0?}
B -- Yes\nisBranchNode=true --> D{is elseNodeType?}
C -- Yes --> E[cyclomatic++]
C -- No --> F[walk children\nnormally]
D -- Yes --> G{first namedChild\nis ifNodeType?}
G -- Yes, else-if --> H[walk children\nat same level]
G -- No, plain else --> I[cognitive++\nwalk children]
D -- No --> J{is elifNodeType?}
J -- Yes --> K[cognitive++\ncyclomatic++\nwalk children]
J -- No --> L{is ifNodeType AND\nisElseIf pattern?}
L -- Yes --> M[cognitive++\ncyclomatic++\nwalk children]
L -- No, regular branch --> N[cognitive += 1+nesting\ncyclomatic++]
N --> O{in switchLikeNodes?}
O -- Yes --> P[cyclomatic--\nnet 0 for container]
O -- No --> Q{in nestingNodes?}
P --> Q
Q -- Yes --> R[walk children\nat nesting+1]
Q -- No --> S[walk children\nat same level]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Node visited] --> B{in branchNodes\nAND childCount > 0?}
B -- No --> C{in caseNodes\nAND childCount > 0?}
B -- Yes\nisBranchNode=true --> D{is elseNodeType?}
C -- Yes --> E[cyclomatic++]
C -- No --> F[walk children\nnormally]
D -- Yes --> G{first namedChild\nis ifNodeType?}
G -- Yes, else-if --> H[walk children\nat same level]
G -- No, plain else --> I[cognitive++\nwalk children]
D -- No --> J{is elifNodeType?}
J -- Yes --> K[cognitive++\ncyclomatic++\nwalk children]
J -- No --> L{is ifNodeType AND\nisElseIf pattern?}
L -- Yes --> M[cognitive++\ncyclomatic++\nwalk children]
L -- No, regular branch --> N[cognitive += 1+nesting\ncyclomatic++]
N --> O{in switchLikeNodes?}
O -- Yes --> P[cyclomatic--\nnet 0 for container]
O -- No --> Q{in nestingNodes?}
P --> Q
Q -- Yes --> R[walk children\nat nesting+1]
Q -- No --> S[walk children\nat same level]
|
| 'while_statement', | ||
| 'do_statement', | ||
| 'case_statement', | ||
| 'conditional_expression', |
There was a problem hiding this comment.
switch_statement in switchLikeNodes is unreachable for C/C++
switchLikeNodes is only consulted inside handleBranchNode / classifyBranchNode, both of which gate on branchNodes.has(type) first. Because switch_statement is not in branchNodes for C or C++, the cyclomatic-decrement logic (acc.cyclomatic--) can never fire for it in either code path. The entry is therefore dead config in the TS engine (and mirrors the same structural quirk in the native C_RULES/CPP_RULES). This is acknowledged as intentional parity pending #2058 — just flagging so a future reader doesn't wonder why adding switch_statement to branchNodes suddenly makes the switchLikeNodes entry "activate".
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Ports the "tier-1" languages identified in #1923 — c, cpp, kotlin, swift, scala, bash already had a native
LangRules/HalsteadRulesdefinition incrates/codegraph-core/src/ast_analysis/complexity.rs, but the WASM/TS side had noCOMPLEXITY_RULES/HALSTEAD_RULESentry at all, socodegraph complexitysilently returnedfunctions: []for these languages under the WASM engine.Added
complexity/complexityCpp+halstead/halsteadCpptosrc/ast-analysis/rules/c.ts,complexityKotlin/complexitySwift/complexityScala(+ Halstead) tob2.ts,complexityBash/halsteadBashtob3.ts, and wired all of them intoCOMPLEXITY_RULES/HALSTEAD_RULESinindex.ts. Also added the matching LOC comment-prefix entries inast-analysis/metrics.ts.Rather than trust the native config as pre-verified, every rule was checked against real tree-sitter output for these six languages (not just read from the Rust source), which surfaced three real, previously-shipping bugs in the native engine — fixed in this PR on both engines since I was already introducing this exact code path to WASM and wasn't going to knowingly port broken behavior into a brand-new integration:
C_RULES/CPP_RULEShadelse_via_alternativebackwards. tree-sitter-c/cpp wrap the else branch in a realelse_clausenode (Pattern A, like JS/C#/Rust) — confirmed by parsing and inspecting the S-expression — not Go/Java's alternative-field pattern the rules assumed. A canonicalif {} else if {} else {}computedcognitive: 5, maxNesting: 2instead of the correctcognitive: 3, maxNesting: 1(same as every other language for the identical shape). Fixedelse_node_type/else_via_alternativeand addedelse_clausetobranch_nodes.BASH_RULESwas missingelse_clausefrombranch_nodes.else_node_type/elif_node_typewere already correct (flat-sibling pattern, same as Python/Ruby/PHP/Lua), butbranch_nodesonly listedelif_clause— a trailing plainelsegot zero cognitive credit.if/elif/elsecomputedcognitive: 2instead of3.SWIFT_RULESusedlogical_node_types: ["binary_expression"], but Swift splits&&/||intoconjunction_expression/disjunction_expression(same as Kotlin) — confirmed by parsinga && b || a. Swift's&&/||were never counted as logical operators at all under the existing config.Also found and fixed a WASM-only divergence from the Rust engine (not a native bug):
computeFunctionComplexity(features/complexity.ts) and the complexity visitor (ast-analysis/visitors/complexity-visitor.ts) evaluatedbranchNodesandcaseNodesindependently, so a node type listed in both sets (Kotlin'swhen_entry, Scala'scase_clause— both real, if imperfect, entries already in the native rules) got double-counted. Rust'swalk()returns unconditionally after abranch_nodesmatch, making thecase_nodescheck unreachable for such a type — awhen/matchwith 3 cases computed cyclomatic7in WASM vs4in native, an immediately obvious cross-engine divergence. Fixed both TS implementations to mirror Rust's exclusivity.Widened
ComplexityRules.logicalNodeType(singularstring | null) tologicalNodeTypes: Set<string>, matching the nativelogical_node_types: &'static [&'static str]slice shape — the old singular field could never represent Kotlin/Swift's split&&/||node types. Updated all 9 existing per-language rule files to the plural field name (mechanical rename, no behavior change for any of them since they all use one shared binary-op node type).Deferred (filed, not fixed here)
Filed #2058 for remaining known-imperfect native rules that this PR intentionally mirrors as-is on the TS side for engine parity — these are pre-existing native behavior, not something this PR introduces, and need methodology judgment calls rather than a mechanical config fix:
case_statement/when_entry/case_clauselisted in bothbranch_nodesandcase_nodes, so each case gets nesting-weighted cognitive credit like a fresh branch instead of a flat+1).branch_nodes/nesting_nodes.else ifchains counting as progressively nested branches instead of a flat chain — verified their grammars have noelse_clausewrapper and noalternativefield (unlike C/C++, which I could fix, and Scala, which already has a realalternativefield). Needs a new sibling-position detection primitive in the sharedclassify_branch/detect_else_ifalgorithm, not a data fix.comment_prefixes()list (["//", "/*"]) missing*/*/continuation-line prefixes, undercounting multi-line block comments for the MI calculation.The remaining 17 tier-2 languages (functional/pattern-matching languages needing from-scratch grammar design per #1923's own scoping — Haskell, OCaml, F#, Clojure, Erlang, Gleam, etc.) are left for follow-up work under #1923, which stays open.
Refs #1923
Test plan
npm test— full suite green (255 files, 4061 passed, 30 skipped, 2 todo, 0 failed)cargo test --release(crates/codegraph-core) — 624 passed, including 17 new tests covering c/cpp/kotlin/swift/scala/bash complexity + the else-clause/else_via_alternative fixesnpm run lint/npm run build— cleantests/unit/complexity.test.ts: per-language suites for C/C++/Kotlin/Swift/Scala/Bash (simple function, single if, if/else-if/else where applicable, logical operators, switch/when/match, Halstead volume, LOC comments), plus a DFS-vs-visitor parity suite covering all six languages including Kotlin's multi-node-type logical operator casenapi build --platform --release), codesigned, and verified directly:codegraph complexity --file <fixture> --engine nativevs--engine wasmproduce byte-identical JSON for real fixture files in all six languages (tests/benchmarks/resolution/fixtures/{c,cpp,kotlin,swift,scala,bash}/service.*)