Skip to content

Native Rust computes trivial complexity for bodyless Rust trait methods; WASM correctly omits them (engine parity gap) #2053

Description

@carlos-alm

Summary

handle_trait_item in crates/codegraph-core/src/extractors/rust_lang.rs calls compute_all_metrics(&child, source, "rust") unconditionally for every trait member, regardless of whether child.kind() is function_signature_item (a required trait method with no default implementation, no body) or function_item (a default-implemented trait method, has a body):

if child.kind() != "function_signature_item" && child.kind() != "function_item" {
    continue;
}
if let Some(meth_name) = child.child_by_field_name("name") {
    symbols.definitions.push(Definition {
        name: format!("{}.{}", trait_name, node_text(&meth_name, source)),
        kind: "method".to_string(),
        line: start_line(&child),
        end_line: Some(end_line(&child)),
        decorators: None,
        complexity: compute_all_metrics(&child, source, "rust"),
        cfg: build_function_cfg(&child, "rust", source),
        children: None,
        ...
    });
}

compute_all_metrics doesn't check whether the passed node's kind is actually a member of the language's own function_nodes rule set — it just walks whatever node it's given, counting branch/nesting node types among its descendants. For a function_signature_item (no body, just fn name(&self, ...) -> T;), this produces a trivial but non-null ComplexityMetrics { cognitive: 0, cyclomatic: 1, ... }, because a bodyless signature has no descendants that look like branches.

Meanwhile, the WASM/TS side's COMPLEXITY_RULES for Rust (src/ast-analysis/rules/rust.ts) deliberately excludes function_signature_item from functionNodes:

functionNodes: new Set(['function_item', 'closure_expression']),

So the WASM complexity visitor never even attempts to compute complexity for a function_signature_item — it produces no complexity entry at all for that definition.

Reproduction

trait Repo {
    fn save(&mut self, id: &str, value: i32) -> bool;   // required, no body

    fn find(&self, id: &str) -> Option<i32> {             // default impl, has body
        if id.is_empty() { return None; }
        for i in 0..3 { if i == 1 { return Some(i); } }
        None
    }
}
$ codegraph complexity --file repo.rs -T --json --engine wasm
functions: 1   [ Repo.find ]

$ codegraph complexity --file repo.rs -T --json --engine native
functions: 2   [ Repo.find, Repo.save ]   ← Repo.save has cyclomatic: 1, cognitive: 0

Per repo convention (CLAUDE.md): "Both engines must produce identical results. If they diverge, the less-accurate engine has a bug." Here WASM is correct (a signature-only trait method has no complexity to report); native is wrong (fabricates a trivial-but-meaningless complexity entry for a function with no body).

Suggested fix

handle_trait_item should skip calling compute_all_metrics/build_function_cfg for function_signature_item children and push complexity: None, cfg: None instead — mirroring how csharp.rs's handle_interface_decl already handles this same situation for C# interface methods (explicit None with a comment: "Interface method declarations have no body — skip CFG and complexity to mirror the WASM extractor").

Discovery context

Found while fixing #1922 (WASM complexity engine skips entire file when every function has a dotted name). That fix added a Definition.bodyless signal (set via child.child_by_field_name("body").is_none()) to both engines so the file-level "does this file need complexity" gate no longer misclassifies real bodied dotted-name functions as signature stubs. While auditing every native extractor site that mirrors a WASM interface/trait stub extractor to set bodyless correctly, rust_lang.rs's trait handling was the only one where the existing complexity/cfg values (not just the new bodyless flag) already diverged from WASM before that fix — an independent, pre-existing bug, not something introduced by #1922's change. Not fixed there to keep that PR scoped to the file-level gate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions