Summary
For C#, native and WASM disagree on whether an interface method signature (no body) should get a complexity entry at all:
crates/codegraph-core/src/extractors/csharp.rs's handle_interface_decl explicitly passes complexity: None, cfg: None for interface methods, with a comment: "Interface method declarations have no body — skip CFG and complexity to mirror the WASM extractor and avoid producing meaningless metrics for body-less declarations."
- But the WASM/TS side's
COMPLEXITY_RULES for C# (src/ast-analysis/rules/csharp.ts) puts method_declaration in functionNodes unconditionally (matching both bodied class methods and bodyless interface signatures, since C# uses the same node type for both):
functionNodes: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression', 'local_function_statement']),
The complexity visitor walks any node matching functionNodes without checking whether it actually has a body — so once the file-level gate lets the visitor run, it computes a trivial-but-non-null { cyclomatic: 1, cognitive: 0 } for the interface stub too.
So the "mirror the WASM extractor" comment in csharp.rs describes intent, not actual current WASM behavor — WASM does not skip bodyless method_declaration nodes; it just walks them and gets a trivial result.
Reproduction
interface IRepo {
bool Save(string id, int value);
}
class Repo : IRepo {
public bool Save(string id, int value) {
if (value < 0) { return false; }
for (int i = 0; i < 3; i++) { if (i == value) { return true; } }
return true;
}
}
$ codegraph complexity --file Repo.cs -T --json --engine native
functions: 1 [ Repo.Save ]
$ codegraph complexity --file Repo.cs -T --json --engine wasm
functions: 2 [ Repo.Save, IRepo.Save ] ← IRepo.Save has cyclomatic: 1, cognitive: 0
Per CLAUDE.md: "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug." Here WASM is the less-accurate engine — it fabricates a complexity entry for a method with no executable body.
Suggested fix
The WASM complexity visitor (src/ast-analysis/visitors/complexity-visitor.ts / the shared walker) needs a real "does this function-node have a body" check before computing metrics — not just node-type membership in functionNodes — so it agrees with native's explicit exclusion for bodyless method_declaration nodes. This likely affects any language where the same node type is used for both bodied and bodyless declarations (C# interfaces; Java interfaces/abstract methods happen to already agree between engines only because native's java.rs also computes unconditionally for these — a related but separate inconsistency worth auditing at the same time).
Discovery context
Found while fixing #1922 (WASM complexity engine skips entire file when every function has a dotted name) — live-testing native vs WASM parity for C# interface + implementing-class fixtures as part of verifying the mirrored Definition.bodyless signal added in that fix. Independent, pre-existing bug in how each engine handles signature-only declarations that happen to share a node type with real methods — not fixed there to keep that PR scoped to the file-level complexity gate. Related to #2053 (same class of "signature-only node computed as if bodied" bug, opposite direction, for Rust trait methods).
Summary
For C#, native and WASM disagree on whether an interface method signature (no body) should get a complexity entry at all:
crates/codegraph-core/src/extractors/csharp.rs'shandle_interface_declexplicitly passescomplexity: None, cfg: Nonefor interface methods, with a comment: "Interface method declarations have no body — skip CFG and complexity to mirror the WASM extractor and avoid producing meaningless metrics for body-less declarations."COMPLEXITY_RULESfor C# (src/ast-analysis/rules/csharp.ts) putsmethod_declarationinfunctionNodesunconditionally (matching both bodied class methods and bodyless interface signatures, since C# uses the same node type for both):functionNodeswithout checking whether it actually has a body — so once the file-level gate lets the visitor run, it computes a trivial-but-non-null{ cyclomatic: 1, cognitive: 0 }for the interface stub too.So the "mirror the WASM extractor" comment in
csharp.rsdescribes intent, not actual current WASM behavor — WASM does not skip bodylessmethod_declarationnodes; it just walks them and gets a trivial result.Reproduction
Per CLAUDE.md: "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug." Here WASM is the less-accurate engine — it fabricates a complexity entry for a method with no executable body.
Suggested fix
The WASM complexity visitor (
src/ast-analysis/visitors/complexity-visitor.ts/ the shared walker) needs a real "does this function-node have a body" check before computing metrics — not just node-type membership infunctionNodes— so it agrees with native's explicit exclusion for bodylessmethod_declarationnodes. This likely affects any language where the same node type is used for both bodied and bodyless declarations (C# interfaces; Java interfaces/abstract methods happen to already agree between engines only because native'sjava.rsalso computes unconditionally for these — a related but separate inconsistency worth auditing at the same time).Discovery context
Found while fixing #1922 (WASM complexity engine skips entire file when every function has a dotted name) — live-testing native vs WASM parity for C# interface + implementing-class fixtures as part of verifying the mirrored
Definition.bodylesssignal added in that fix. Independent, pre-existing bug in how each engine handles signature-only declarations that happen to share a node type with real methods — not fixed there to keep that PR scoped to the file-level complexity gate. Related to #2053 (same class of "signature-only node computed as if bodied" bug, opposite direction, for Rust trait methods).