From 281edf8bc4673c9887746eabe935e15365111106 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Wed, 8 Jul 2026 12:14:17 -0600 Subject: [PATCH] fix(complexity): stop using dotted names as a proxy for signature-only stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasFuncBody() (and its duplicated logic in features/complexity.ts and features/cfg.ts) excluded any definition whose name contained a dot, intended to filter out bodyless TS interface method signatures. Dotted names are also the normal qualified name for real, bodied class/struct/ impl/module methods in every extractor (Lua's `M.foo` module-table pattern, Go/Java/C#/PHP/Rust receiver or impl methods, any `Class.method` in any language) — so when *every* function/method in a file happened to have a dotted name, the file-level "does this file need WASM complexity" gate (`defs.some(hasFuncBody)`) went false for the whole file, and none of its functions got complexity or CFG data. Replace the name-shape heuristic with `Definition.bodyless`, a direct signal each extractor sets from the AST node's body field at the exact sites that produce genuinely bodyless dotted definitions (TS/JS interface method_signature, Go interface method_elem, Java/C#/PHP interface and abstract methods, Rust trait function_signature_item). `hasFuncBody`, `initWasmParsersIfNeeded`, `classifyDefinitionForNativeBulk`, and `hasNativeCfgForFile` now check `!d.bodyless` instead of `!d.name.includes('.')`; the latter two were also folded onto the shared `hasFuncBody` to remove duplicated logic. Mirrors the same fix into the native Rust engine's `Definition` struct and every extractor that produces dotted names, so both engines agree on which definitions are real (bodied) vs. signature-only. Native already computed complexity unconditionally and was unaffected by the original bug, but without `bodyless` its bridged Definitions would have looked identical to WASM's pre-fix state to the JS-side gate, silently forcing unnecessary WASM/JS fallback re-analysis on every native build touching an interface, trait, or abstract method. Fixes #1922 Impact: 14 functions changed, 54 affected --- .../graph/builder/stages/import_edges.rs | 1 + crates/codegraph-core/src/extractors/bash.rs | 1 + crates/codegraph-core/src/extractors/c.rs | 5 + .../codegraph-core/src/extractors/clojure.rs | 5 + crates/codegraph-core/src/extractors/cpp.rs | 6 + .../codegraph-core/src/extractors/csharp.rs | 8 + crates/codegraph-core/src/extractors/cuda.rs | 6 + crates/codegraph-core/src/extractors/dart.rs | 7 + .../codegraph-core/src/extractors/elixir.rs | 4 + .../codegraph-core/src/extractors/erlang.rs | 5 + .../codegraph-core/src/extractors/fsharp.rs | 5 + crates/codegraph-core/src/extractors/gleam.rs | 5 + crates/codegraph-core/src/extractors/go.rs | 38 ++++ .../codegraph-core/src/extractors/groovy.rs | 6 + .../codegraph-core/src/extractors/haskell.rs | 7 + crates/codegraph-core/src/extractors/hcl.rs | 2 + .../codegraph-core/src/extractors/helpers.rs | 1 + crates/codegraph-core/src/extractors/java.rs | 42 +++++ .../src/extractors/javascript.rs | 61 ++++++ crates/codegraph-core/src/extractors/julia.rs | 7 + .../codegraph-core/src/extractors/kotlin.rs | 5 + crates/codegraph-core/src/extractors/lua.rs | 1 + crates/codegraph-core/src/extractors/objc.rs | 8 + crates/codegraph-core/src/extractors/ocaml.rs | 9 + crates/codegraph-core/src/extractors/php.rs | 7 + .../codegraph-core/src/extractors/python.rs | 3 + .../codegraph-core/src/extractors/r_lang.rs | 4 + crates/codegraph-core/src/extractors/ruby.rs | 5 + .../src/extractors/rust_lang.rs | 27 +++ crates/codegraph-core/src/extractors/scala.rs | 4 + .../codegraph-core/src/extractors/solidity.rs | 11 ++ crates/codegraph-core/src/extractors/swift.rs | 4 + .../codegraph-core/src/extractors/verilog.rs | 6 + crates/codegraph-core/src/extractors/zig.rs | 4 + crates/codegraph-core/src/types.rs | 8 + src/ast-analysis/apply-results.ts | 16 +- src/extractors/csharp.ts | 2 + src/extractors/go.ts | 2 + src/extractors/java.ts | 2 + src/extractors/javascript.ts | 1 + src/extractors/php.ts | 2 + src/extractors/rust.ts | 1 + src/features/cfg.ts | 10 +- src/features/complexity.ts | 26 ++- src/types.ts | 10 + ...-1922-wasm-complexity-dotted-names.test.ts | 175 ++++++++++++++++++ tests/unit/apply-results.test.ts | 21 ++- 47 files changed, 568 insertions(+), 28 deletions(-) create mode 100644 tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs index 9a6d74430..db058aa03 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs @@ -746,6 +746,7 @@ mod tests { complexity: None, cfg: None, children: None, + bodyless: None, }) .collect(), imports, diff --git a/crates/codegraph-core/src/extractors/bash.rs b/crates/codegraph-core/src/extractors/bash.rs index d0e3fec72..d8e30213c 100644 --- a/crates/codegraph-core/src/extractors/bash.rs +++ b/crates/codegraph-core/src/extractors/bash.rs @@ -30,6 +30,7 @@ fn match_bash_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth complexity: compute_all_metrics(node, source, "bash"), cfg: build_function_cfg(node, "bash", source), children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/c.rs b/crates/codegraph-core/src/extractors/c.rs index 0fc5310b0..fb5c926fa 100644 --- a/crates/codegraph-core/src/extractors/c.rs +++ b/crates/codegraph-core/src/extractors/c.rs @@ -193,6 +193,7 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u complexity: compute_all_metrics(node, source, "c"), cfg: build_function_cfg(node, "c", source), children: opt_children(children), + bodyless: None, }); } } @@ -212,6 +213,7 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -230,6 +232,7 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -246,6 +249,7 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -274,6 +278,7 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/clojure.rs b/crates/codegraph-core/src/extractors/clojure.rs index 160bb5f9e..710642985 100644 --- a/crates/codegraph-core/src/extractors/clojure.rs +++ b/crates/codegraph-core/src/extractors/clojure.rs @@ -190,6 +190,7 @@ fn handle_ns_form(node: &Node, source: &[u8], symbols: &mut FileSymbols) -> Opti complexity: None, cfg: None, children: None, + bodyless: None, }); // Scan for nested `(:require ...)`, `(:import ...)`, `(:use ...)` forms. @@ -269,6 +270,7 @@ fn handle_def_form( complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -302,6 +304,7 @@ fn handle_defn_form( complexity: compute_all_metrics(node, source, "clojure"), cfg: build_function_cfg(node, "clojure", source), children: opt_children(params), + bodyless: None, }); } @@ -351,6 +354,7 @@ fn handle_defprotocol(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -368,6 +372,7 @@ fn handle_defrecord(node: &Node, source: &[u8], symbols: &mut FileSymbols, kind: complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/cpp.rs b/crates/codegraph-core/src/extractors/cpp.rs index cc693c807..55682aafc 100644 --- a/crates/codegraph-core/src/extractors/cpp.rs +++ b/crates/codegraph-core/src/extractors/cpp.rs @@ -203,6 +203,7 @@ fn handle_cpp_function_definition(node: &Node, source: &[u8], symbols: &mut File complexity: compute_all_metrics(node, source, "cpp"), cfg: build_function_cfg(node, "cpp", source), children: opt_children(children), + bodyless: None, }); } } @@ -222,6 +223,7 @@ fn handle_cpp_class_specifier(node: &Node, source: &[u8], symbols: &mut FileSymb complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_cpp_base_classes(node, source, &class_name, symbols); } @@ -242,6 +244,7 @@ fn handle_cpp_struct_specifier(node: &Node, source: &[u8], symbols: &mut FileSym complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_cpp_base_classes(node, source, &struct_name, symbols); } @@ -259,6 +262,7 @@ fn handle_cpp_enum_specifier(node: &Node, source: &[u8], symbols: &mut FileSymbo complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -274,6 +278,7 @@ fn handle_cpp_namespace_definition(node: &Node, source: &[u8], symbols: &mut Fil complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -301,6 +306,7 @@ fn handle_cpp_type_definition(node: &Node, source: &[u8], symbols: &mut FileSymb complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/csharp.rs b/crates/codegraph-core/src/extractors/csharp.rs index 8d3f45aec..a32e2fdca 100644 --- a/crates/codegraph-core/src/extractors/csharp.rs +++ b/crates/codegraph-core/src/extractors/csharp.rs @@ -61,6 +61,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_csharp_base_types(node, &class_name, source, symbols); } @@ -77,6 +78,7 @@ fn handle_struct_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); extract_csharp_base_types(node, &name, source, symbols); } @@ -93,6 +95,7 @@ fn handle_record_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); extract_csharp_base_types(node, &name, source, symbols); } @@ -109,6 +112,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); if let Some(body) = node.child_by_field_name("body") { for i in 0..body.child_count() { @@ -127,6 +131,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: Some(child.child_by_field_name("body").is_none()), }); } } @@ -146,6 +151,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -168,6 +174,7 @@ fn handle_method_or_ctor(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: compute_all_metrics(node, source, "csharp"), cfg: build_function_cfg(node, "csharp", source), children: opt_children(children), + bodyless: Some(node.child_by_field_name("body").is_none()), }); } @@ -204,6 +211,7 @@ fn handle_property_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/cuda.rs b/crates/codegraph-core/src/extractors/cuda.rs index 2ce25ef53..abc9a5909 100644 --- a/crates/codegraph-core/src/extractors/cuda.rs +++ b/crates/codegraph-core/src/extractors/cuda.rs @@ -362,6 +362,7 @@ fn handle_cuda_function_definition(node: &Node, source: &[u8], symbols: &mut Fil complexity: compute_all_metrics(node, source, "cpp"), cfg: build_function_cfg(node, "cpp", source), children: opt_children(children), + bodyless: None, }); } } @@ -382,6 +383,7 @@ fn handle_cuda_class_specifier(node: &Node, source: &[u8], symbols: &mut FileSym complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_cuda_base_classes(node, source, &class_name, symbols); } @@ -403,6 +405,7 @@ fn handle_cuda_struct_specifier(node: &Node, source: &[u8], symbols: &mut FileSy complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -419,6 +422,7 @@ fn handle_cuda_enum_specifier(node: &Node, source: &[u8], symbols: &mut FileSymb complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -434,6 +438,7 @@ fn handle_cuda_namespace_definition(node: &Node, source: &[u8], symbols: &mut Fi complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -464,6 +469,7 @@ fn handle_cuda_type_definition(node: &Node, source: &[u8], symbols: &mut FileSym complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/dart.rs b/crates/codegraph-core/src/extractors/dart.rs index 29d36e042..cda617039 100644 --- a/crates/codegraph-core/src/extractors/dart.rs +++ b/crates/codegraph-core/src/extractors/dart.rs @@ -92,6 +92,7 @@ fn handle_dart_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -148,6 +149,7 @@ fn extract_dart_class_methods(body: &Node, class_name: &str, source: &[u8], symb complexity: compute_all_metrics(&sig, source, "dart"), cfg: build_function_cfg(&sig, "dart", source), children: None, + bodyless: None, }); } } @@ -202,6 +204,7 @@ fn handle_dart_enum(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -220,6 +223,7 @@ fn handle_dart_mixin(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -238,6 +242,7 @@ fn handle_dart_extension(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -256,6 +261,7 @@ fn handle_dart_function_sig(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: compute_all_metrics(node, source, "dart"), cfg: build_function_cfg(node, "dart", source), children: None, + bodyless: None, }); } @@ -305,6 +311,7 @@ fn handle_dart_type_alias(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/elixir.rs b/crates/codegraph-core/src/extractors/elixir.rs index 237d62449..51527966b 100644 --- a/crates/codegraph-core/src/extractors/elixir.rs +++ b/crates/codegraph-core/src/extractors/elixir.rs @@ -73,6 +73,7 @@ fn handle_defmodule(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -142,6 +143,7 @@ fn handle_def_function(node: &Node, source: &[u8], symbols: &mut FileSymbols, _k complexity: compute_all_metrics(node, source, "elixir"), cfg: build_function_cfg(node, "elixir", source), children: opt_children(params), + bodyless: None, }); } @@ -325,6 +327,7 @@ fn handle_defprotocol(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -347,6 +350,7 @@ fn handle_defimpl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/erlang.rs b/crates/codegraph-core/src/extractors/erlang.rs index 8d1d07b4a..68f74a07d 100644 --- a/crates/codegraph-core/src/extractors/erlang.rs +++ b/crates/codegraph-core/src/extractors/erlang.rs @@ -56,6 +56,7 @@ fn handle_module_attr(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -97,6 +98,7 @@ fn handle_record_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -123,6 +125,7 @@ fn handle_type_alias(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -175,6 +178,7 @@ fn handle_function_clause(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: opt_children(params), + bodyless: None, }); } @@ -244,6 +248,7 @@ fn handle_define(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/fsharp.rs b/crates/codegraph-core/src/extractors/fsharp.rs index f9a442701..24a4e6ab1 100644 --- a/crates/codegraph-core/src/extractors/fsharp.rs +++ b/crates/codegraph-core/src/extractors/fsharp.rs @@ -80,6 +80,7 @@ fn handle_named_module(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -110,6 +111,7 @@ fn handle_module_defn(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -157,6 +159,7 @@ fn handle_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(&end, source, "fsharp"), cfg: build_function_cfg(&end, "fsharp", source), children: opt_children(params), + bodyless: None, }); } @@ -226,6 +229,7 @@ fn handle_type_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -406,6 +410,7 @@ fn handle_value_definition(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/gleam.rs b/crates/codegraph-core/src/extractors/gleam.rs index db29a2b13..2145e8f88 100644 --- a/crates/codegraph-core/src/extractors/gleam.rs +++ b/crates/codegraph-core/src/extractors/gleam.rs @@ -49,6 +49,7 @@ fn handle_function(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "gleam"), cfg: build_function_cfg(node, "gleam", source), children: opt_children(params), + bodyless: None, }); } @@ -72,6 +73,7 @@ fn handle_external_function(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: None, cfg: None, children: opt_children(params), + bodyless: None, }); } @@ -139,6 +141,7 @@ fn handle_type_definition(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -160,6 +163,7 @@ fn handle_type_alias(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -181,6 +185,7 @@ fn handle_constant(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/go.rs b/crates/codegraph-core/src/extractors/go.rs index 794ba200e..675f29b16 100644 --- a/crates/codegraph-core/src/extractors/go.rs +++ b/crates/codegraph-core/src/extractors/go.rs @@ -44,6 +44,7 @@ fn handle_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "go"), cfg: build_function_cfg(node, "go", source), children: opt_children(children), + bodyless: None, }); } } @@ -66,6 +67,7 @@ fn handle_method_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "go"), cfg: build_function_cfg(node, "go", source), children: opt_children(children), + bodyless: Some(node.child_by_field_name("body").is_none()), }); } @@ -105,6 +107,7 @@ fn handle_type_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } "interface_type" => { @@ -117,6 +120,7 @@ fn handle_type_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); extract_go_interface_methods(&type_node, &name, source, symbols); } @@ -130,6 +134,7 @@ fn handle_type_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -150,6 +155,7 @@ fn extract_go_interface_methods(type_node: &Node, iface_name: &str, source: &[u8 complexity: None, cfg: None, children: None, + bodyless: Some(member.child_by_field_name("body").is_none()), }); } } @@ -169,6 +175,7 @@ fn handle_const_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -501,6 +508,10 @@ mod tests { let names: Vec<&str> = s.definitions.iter().map(|d| d.name.as_str()).collect(); assert!(names.contains(&"Server")); assert!(names.contains(&"Server.Start")); + // A real receiver method has a body — must not be marked bodyless (#1922: + // a dotted name alone must never be treated as a signature-only stub). + let start = s.definitions.iter().find(|d| d.name == "Server.Start").unwrap(); + assert_ne!(start.bodyless, Some(true)); } #[test] @@ -509,6 +520,33 @@ mod tests { let names: Vec<&str> = s.definitions.iter().map(|d| d.name.as_str()).collect(); assert!(names.contains(&"Reader")); assert!(names.contains(&"Reader.Read")); + // An interface method_elem structurally has no body field — must be marked + // bodyless so the WASM/native "needs complexity" gate correctly skips it + // instead of forcing an unnecessary fallback (#1922). + let read = s.definitions.iter().find(|d| d.name == "Reader.Read").unwrap(); + assert_eq!(read.bodyless, Some(true)); + } + + #[test] + fn receiver_only_file_has_no_bodyless_methods() { + // Reproduces the exact #1922 report: a file with only receiver methods + // (no free functions), each with a real body. + let s = parse_go( + "package main\n\ + type Repo struct{ items map[string]int }\n\ + func (r *Repo) Save(id string, v int) bool { return true }\n\ + func (r *Repo) Find(id string) (int, bool) { return 0, false }\n", + ); + let methods: Vec<_> = s + .definitions + .iter() + .filter(|d| d.kind == "method") + .collect(); + assert_eq!(methods.len(), 2); + for m in methods { + assert_ne!(m.bodyless, Some(true), "{} should not be bodyless", m.name); + assert!(m.complexity.is_some(), "{} should have complexity computed", m.name); + } } #[test] diff --git a/crates/codegraph-core/src/extractors/groovy.rs b/crates/codegraph-core/src/extractors/groovy.rs index 0c59aa10c..2fca0b3b3 100644 --- a/crates/codegraph-core/src/extractors/groovy.rs +++ b/crates/codegraph-core/src/extractors/groovy.rs @@ -83,6 +83,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); // Superclass: `superclass` field wraps a `_type` child (type_identifier / @@ -168,6 +169,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); // `interface X extends Y, Z` — tree-sitter-groovy 0.1.x exposes parent @@ -212,6 +214,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(members), + bodyless: None, }); } @@ -235,6 +238,7 @@ fn handle_method_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "groovy"), cfg: build_function_cfg(node, "groovy", source), children: opt_children(params), + bodyless: None, }); } @@ -256,6 +260,7 @@ fn handle_constructor_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: compute_all_metrics(node, source, "groovy"), cfg: build_function_cfg(node, "groovy", source), children: opt_children(params), + bodyless: None, }); } @@ -272,6 +277,7 @@ fn handle_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "groovy"), cfg: build_function_cfg(node, "groovy", source), children: opt_children(params), + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/haskell.rs b/crates/codegraph-core/src/extractors/haskell.rs index dea5cfb7e..30686518f 100644 --- a/crates/codegraph-core/src/extractors/haskell.rs +++ b/crates/codegraph-core/src/extractors/haskell.rs @@ -48,6 +48,7 @@ fn handle_haskell_function(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: compute_all_metrics(node, source, "haskell"), cfg: build_function_cfg(node, "haskell", source), children: opt_children(params), + bodyless: None, }); } @@ -136,6 +137,7 @@ fn handle_haskell_bind(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -174,6 +176,7 @@ fn handle_haskell_data_type(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -192,6 +195,7 @@ fn handle_haskell_newtype(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -210,6 +214,7 @@ fn handle_haskell_type_synonym(node: &Node, source: &[u8], symbols: &mut FileSym complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -228,6 +233,7 @@ fn handle_haskell_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -246,6 +252,7 @@ fn handle_haskell_instance(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/hcl.rs b/crates/codegraph-core/src/extractors/hcl.rs index 23a2ea4f1..4aebf9bac 100644 --- a/crates/codegraph-core/src/extractors/hcl.rs +++ b/crates/codegraph-core/src/extractors/hcl.rs @@ -69,6 +69,7 @@ fn extract_block_attributes(node: &Node, source: &[u8]) -> Vec { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -124,6 +125,7 @@ fn match_hcl_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: complexity: None, cfg: None, children, + bodyless: None, }); if block_type == "module" { extract_module_source(node, source, symbols); diff --git a/crates/codegraph-core/src/extractors/helpers.rs b/crates/codegraph-core/src/extractors/helpers.rs index 638cc2ad3..8d989dc49 100644 --- a/crates/codegraph-core/src/extractors/helpers.rs +++ b/crates/codegraph-core/src/extractors/helpers.rs @@ -25,6 +25,7 @@ pub fn child_def(name: String, kind: &str, line: u32) -> Definition { complexity: None, cfg: None, children: None, + bodyless: None, } } diff --git a/crates/codegraph-core/src/extractors/java.rs b/crates/codegraph-core/src/extractors/java.rs index 726db2bf9..d9547737d 100644 --- a/crates/codegraph-core/src/extractors/java.rs +++ b/crates/codegraph-core/src/extractors/java.rs @@ -101,6 +101,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); // Superclass @@ -155,6 +156,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); if let Some(body) = node.child_by_field_name("body") { for i in 0..body.child_count() { @@ -170,6 +172,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: compute_all_metrics(&child, source, "java"), cfg: build_function_cfg(&child, "java", source), children: None, + bodyless: Some(child.child_by_field_name("body").is_none()), }); } } @@ -189,6 +192,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -221,6 +225,7 @@ fn handle_method_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "java"), cfg: build_function_cfg(node, "java", source), children: opt_children(children), + bodyless: Some(node.child_by_field_name("body").is_none()), }); } } @@ -243,6 +248,7 @@ fn handle_constructor_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: compute_all_metrics(node, source, "java"), cfg: build_function_cfg(node, "java", source), children: opt_children(children), + bodyless: None, }); } } @@ -524,6 +530,42 @@ mod tests { assert_eq!(children[0].name, "x"); assert_eq!(children[0].kind, "parameter"); assert_eq!(children[1].name, "y"); + // A real, bodied method — a dotted name alone must never be treated as a + // signature-only stub (#1922). + assert_ne!(bar.bodyless, Some(true)); + } + + /// Regression test for #1922: an interface method declaration (no body) must + /// be marked `bodyless`; an abstract class method (also no body, but reached + /// via the generic `handle_method_decl` path, not the interface-specific one) + /// must be marked `bodyless` too — the same dotted-name shape as a real + /// method must not be enough to tell them apart. + #[test] + fn interface_and_abstract_methods_are_bodyless_concrete_methods_are_not() { + let iface = parse_java("interface Repo { boolean save(String id, int value); }"); + let iface_save = iface + .definitions + .iter() + .find(|d| d.name == "Repo.save") + .unwrap(); + assert_eq!(iface_save.bodyless, Some(true)); + + let abstract_cls = + parse_java("abstract class Repo { abstract boolean save(String id, int value); }"); + let abstract_save = abstract_cls + .definitions + .iter() + .find(|d| d.name == "Repo.save") + .unwrap(); + assert_eq!(abstract_save.bodyless, Some(true)); + + let concrete = parse_java("class Repo { boolean save(String id, int value) { return true; } }"); + let concrete_save = concrete + .definitions + .iter() + .find(|d| d.name == "Repo.save") + .unwrap(); + assert_ne!(concrete_save.bodyless, Some(true)); } #[test] diff --git a/crates/codegraph-core/src/extractors/javascript.rs b/crates/codegraph-core/src/extractors/javascript.rs index 4469dd01f..092407f4b 100644 --- a/crates/codegraph-core/src/extractors/javascript.rs +++ b/crates/codegraph-core/src/extractors/javascript.rs @@ -571,6 +571,7 @@ fn extract_object_literal_functions( complexity: compute_all_metrics(&val_n, source, "javascript"), cfg: build_function_cfg(&val_n, "javascript", source), children: None, + bodyless: None, }); // Store qualified name as value so resolver looks up the qualified def. symbols.type_map.push(TypeMapEntry { @@ -618,6 +619,7 @@ fn extract_object_literal_functions( complexity: compute_all_metrics(&child, source, "javascript"), cfg: build_function_cfg(&child, "javascript", source), children: opt_children(children), + bodyless: None, }); } let body = child.child_by_field_name("body"); @@ -630,6 +632,7 @@ fn extract_object_literal_functions( complexity: body.and_then(|b| compute_all_metrics(&b, source, "javascript")), cfg: body.and_then(|b| build_function_cfg(&b, "javascript", source)), children: None, + bodyless: None, }); } _ => {} @@ -891,6 +894,7 @@ fn handle_js_prototype_assignment(lhs: &Node, rhs: &Node, source: &[u8], symbols complexity: compute_all_metrics(rhs, source, "javascript"), cfg: build_function_cfg(rhs, "javascript", source), children: opt_children(children), + bodyless: None, }); } } @@ -912,6 +916,7 @@ fn emit_js_prototype_method(class_name: &str, method_name: &str, rhs: &Node, sou complexity: compute_all_metrics(rhs, source, "javascript"), cfg: build_function_cfg(rhs, "javascript", source), children: opt_children(children), + bodyless: None, }); } "identifier" => { @@ -943,6 +948,7 @@ fn extract_js_prototype_object_literal(class_name: &str, obj_node: &Node, source complexity: compute_all_metrics(&child, source, "javascript"), cfg: build_function_cfg(&child, "javascript", source), children: opt_children(children), + bodyless: None, }); } "shorthand_property_identifier" => { @@ -1062,6 +1068,7 @@ fn handle_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "javascript"), cfg: build_function_cfg(node, "javascript", source), children: opt_children(children), + bodyless: None, }); } } @@ -1079,6 +1086,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); // Heritage: extends + implements @@ -1180,6 +1188,7 @@ fn handle_method_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "javascript"), cfg: build_function_cfg(node, "javascript", source), children: opt_children(children), + bodyless: None, }); } } @@ -1369,6 +1378,7 @@ fn handle_static_block(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -1407,6 +1417,7 @@ fn handle_field_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -1422,6 +1433,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); // Extract interface methods let body = node @@ -1444,6 +1456,7 @@ fn handle_type_alias(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -1461,6 +1474,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -1497,6 +1511,7 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(&value_n, source, "javascript"), cfg: build_function_cfg(&value_n, "javascript", source), children: opt_children(children), + bodyless: None, }); } else if is_const && name_n.kind() == "object_pattern" && !in_function_scope { // Parity with TS query path (extractDestructuredBindingsWalk): @@ -1548,6 +1563,7 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); // Phase 8.3f: extract function/arrow properties from object literals and seed // typeMap composite keys so that this.method() inside Object.defineProperty @@ -2395,6 +2411,7 @@ fn extract_interface_methods( complexity: None, cfg: None, children: None, + bodyless: Some(child.child_by_field_name("body").is_none()), }); } } @@ -3003,6 +3020,7 @@ fn extract_destructured_bindings( complexity: None, cfg: None, children: None, + bodyless: None, }); } "pair_pattern" | "pair" => { @@ -3019,6 +3037,7 @@ fn extract_destructured_bindings( complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -3056,6 +3075,7 @@ fn extract_array_pattern_bindings( complexity: None, cfg: None, children: None, + bodyless: None, }); } "assignment_pattern" => { @@ -3070,6 +3090,7 @@ fn extract_array_pattern_bindings( complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -3091,6 +3112,7 @@ fn extract_array_pattern_bindings( complexity: None, cfg: None, children: None, + bodyless: None, }); break; } @@ -3482,6 +3504,7 @@ fn extract_callback_definition(call_node: &Node, source: &[u8]) -> Option Option Option O complexity: None, cfg: None, children: None, + bodyless: None, }); Some(name) @@ -118,6 +119,7 @@ fn handle_function_def( complexity: compute_all_metrics(node, source, "julia"), cfg: build_function_cfg(node, "julia", source), children: opt_children(params), + bodyless: None, }); return; } @@ -145,6 +147,7 @@ fn handle_function_def( complexity: compute_all_metrics(node, source, "julia"), cfg: build_function_cfg(node, "julia", source), children: None, + bodyless: None, }); } @@ -185,6 +188,7 @@ fn handle_assignment( complexity: compute_all_metrics(node, source, "julia"), cfg: build_function_cfg(node, "julia", source), children: opt_children(params), + bodyless: None, }); } @@ -266,6 +270,7 @@ fn handle_struct_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -298,6 +303,7 @@ fn handle_abstract_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -376,6 +382,7 @@ fn handle_macro_def( complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/kotlin.rs b/crates/codegraph-core/src/extractors/kotlin.rs index 1a2c2f6b9..c6a8b87a7 100644 --- a/crates/codegraph-core/src/extractors/kotlin.rs +++ b/crates/codegraph-core/src/extractors/kotlin.rs @@ -225,6 +225,7 @@ fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep complexity: None, cfg: None, children: None, + bodyless: None, }); } else if is_kotlin_enum(node) { let children = extract_kotlin_enum_entries(node, source); @@ -237,6 +238,7 @@ fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } else { let children = extract_kotlin_class_properties(node, source); @@ -249,6 +251,7 @@ fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } @@ -269,6 +272,7 @@ fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_kotlin_delegation_specifiers(node, source, &obj_name, symbols); } @@ -293,6 +297,7 @@ fn match_kotlin_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep complexity: compute_all_metrics(node, source, "kotlin"), cfg: build_function_cfg(node, "kotlin", source), children: opt_children(children), + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/lua.rs b/crates/codegraph-core/src/extractors/lua.rs index 54f46c39c..db52be604 100644 --- a/crates/codegraph-core/src/extractors/lua.rs +++ b/crates/codegraph-core/src/extractors/lua.rs @@ -83,6 +83,7 @@ fn handle_lua_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: compute_all_metrics(node, source, "lua"), cfg: build_function_cfg(node, "lua", source), children: opt_children(params), + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/objc.rs b/crates/codegraph-core/src/extractors/objc.rs index 0bf3daa2e..389b8a1d8 100644 --- a/crates/codegraph-core/src/extractors/objc.rs +++ b/crates/codegraph-core/src/extractors/objc.rs @@ -72,6 +72,7 @@ fn handle_class_interface(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: opt_children(members), + bodyless: None, }); // Superclass — use the bare class name (categories already recorded above) @@ -133,6 +134,7 @@ fn handle_class_implementation(node: &Node, source: &[u8], symbols: &mut FileSym complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -151,6 +153,7 @@ fn handle_protocol_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -177,6 +180,7 @@ fn handle_method(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "objc"), cfg: build_function_cfg(node, "objc", source), children: opt_children(params), + bodyless: None, }); } @@ -195,6 +199,7 @@ fn handle_function_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "objc"), cfg: build_function_cfg(node, "objc", source), children: opt_children(params), + bodyless: None, }); } @@ -243,6 +248,7 @@ fn handle_struct_specifier(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -258,6 +264,7 @@ fn handle_enum_specifier(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -285,6 +292,7 @@ fn handle_typedef(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/ocaml.rs b/crates/codegraph-core/src/extractors/ocaml.rs index 38935793b..e3762b4af 100644 --- a/crates/codegraph-core/src/extractors/ocaml.rs +++ b/crates/codegraph-core/src/extractors/ocaml.rs @@ -67,6 +67,7 @@ fn handle_ocaml_let_binding(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: compute_all_metrics(node, source, "ocaml"), cfg: build_function_cfg(node, "ocaml", source), children: None, + bodyless: None, }); } else { symbols.definitions.push(Definition { @@ -78,6 +79,7 @@ fn handle_ocaml_let_binding(node: &Node, source: &[u8], symbols: &mut FileSymbol complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -124,6 +126,7 @@ fn handle_ocaml_module_def(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -151,6 +154,7 @@ fn handle_ocaml_type_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -193,6 +197,7 @@ fn handle_ocaml_class_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -235,6 +240,7 @@ fn handle_ocaml_value_spec(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -253,6 +259,7 @@ fn handle_ocaml_external(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -270,6 +277,7 @@ fn handle_ocaml_module_type_def(node: &Node, source: &[u8], symbols: &mut FileSy complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -294,6 +302,7 @@ fn handle_ocaml_exception_def(node: &Node, source: &[u8], symbols: &mut FileSymb complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/php.rs b/crates/codegraph-core/src/extractors/php.rs index 1feb71818..f2dd60d6c 100644 --- a/crates/codegraph-core/src/extractors/php.rs +++ b/crates/codegraph-core/src/extractors/php.rs @@ -55,6 +55,7 @@ fn handle_function_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "php"), cfg: build_function_cfg(node, "php", source), children: opt_children(children), + bodyless: None, }); } } @@ -72,6 +73,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); // Extends @@ -124,6 +126,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); if let Some(body) = node.child_by_field_name("body") { for i in 0..body.child_count() { @@ -139,6 +142,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: compute_all_metrics(&child, source, "php"), cfg: build_function_cfg(&child, "php", source), children: None, + bodyless: Some(child.child_by_field_name("body").is_none()), }); } } @@ -156,6 +160,7 @@ fn handle_trait_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -173,6 +178,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -195,6 +201,7 @@ fn handle_method_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "php"), cfg: build_function_cfg(node, "php", source), children: opt_children(children), + bodyless: Some(node.child_by_field_name("body").is_none()), }); } } diff --git a/crates/codegraph-core/src/extractors/python.rs b/crates/codegraph-core/src/extractors/python.rs index b0f46cee8..6d8ec4dbe 100644 --- a/crates/codegraph-core/src/extractors/python.rs +++ b/crates/codegraph-core/src/extractors/python.rs @@ -56,6 +56,7 @@ fn handle_function_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "python"), cfg: build_function_cfg(node, "python", source), children: opt_children(children), + bodyless: None, }); } @@ -72,6 +73,7 @@ fn handle_class_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); let superclasses = node .child_by_field_name("superclasses") @@ -109,6 +111,7 @@ fn handle_expr_stmt(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/r_lang.rs b/crates/codegraph-core/src/extractors/r_lang.rs index 4a7081013..896eef281 100644 --- a/crates/codegraph-core/src/extractors/r_lang.rs +++ b/crates/codegraph-core/src/extractors/r_lang.rs @@ -70,6 +70,7 @@ fn handle_binary_op(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(&rhs, source, "r"), cfg: build_function_cfg(&rhs, "r", source), children: opt_children(params), + bodyless: None, }); } else if is_program_level(node) { // Only record top-level variable assignments (matches JS extractor). @@ -82,6 +83,7 @@ fn handle_binary_op(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -299,6 +301,7 @@ fn handle_set_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -314,6 +317,7 @@ fn handle_set_generic(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/ruby.rs b/crates/codegraph-core/src/extractors/ruby.rs index 61bac796b..89a58f4d8 100644 --- a/crates/codegraph-core/src/extractors/ruby.rs +++ b/crates/codegraph-core/src/extractors/ruby.rs @@ -55,6 +55,7 @@ fn handle_assignment(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -73,6 +74,7 @@ fn handle_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); if let Some(superclass) = node.child_by_field_name("superclass") { extract_ruby_superclass(&superclass, &class_name, node, source, symbols); @@ -90,6 +92,7 @@ fn handle_module(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -112,6 +115,7 @@ fn handle_method(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "ruby"), cfg: build_function_cfg(node, "ruby", source), children: opt_children(children), + bodyless: None, }); } @@ -133,6 +137,7 @@ fn handle_singleton_method(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: compute_all_metrics(node, source, "ruby"), cfg: build_function_cfg(node, "ruby", source), children: opt_children(children), + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/rust_lang.rs b/crates/codegraph-core/src/extractors/rust_lang.rs index a215e43c6..7a761e046 100644 --- a/crates/codegraph-core/src/extractors/rust_lang.rs +++ b/crates/codegraph-core/src/extractors/rust_lang.rs @@ -77,6 +77,7 @@ fn handle_function_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "rust"), cfg: build_function_cfg(node, "rust", source), children: opt_children(children), + bodyless: None, }); } @@ -93,6 +94,7 @@ fn handle_struct_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); seed_rust_struct_field_types(node, &struct_name, source, symbols); } @@ -130,6 +132,7 @@ fn handle_enum_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -145,6 +148,7 @@ fn handle_const_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } } @@ -161,6 +165,7 @@ fn handle_trait_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); if let Some(body) = node.child_by_field_name("body") { for i in 0..body.child_count() { @@ -178,6 +183,7 @@ fn handle_trait_item(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(&child, source, "rust"), cfg: build_function_cfg(&child, "rust", source), children: None, + bodyless: Some(child.kind() == "function_signature_item"), }); } } @@ -594,6 +600,27 @@ mod tests { assert_eq!(children[1].name, "b"); } + /// Regression test for #1922: a trait's required method (`function_signature_item`, + /// no default implementation) has no body and must be marked `bodyless`; a + /// default-implemented trait method (`function_item`, has a body) must not be. + #[test] + fn trait_required_method_is_bodyless_default_method_is_not() { + let s = parse_rust( + "trait Repo {\n\ + fn save(&mut self, id: &str, value: i32) -> bool;\n\ + fn find(&self, id: &str) -> Option {\n\ + if id.is_empty() { return None; }\n\ + None\n\ + }\n\ + }\n", + ); + let save = s.definitions.iter().find(|d| d.name == "Repo.save").unwrap(); + assert_eq!(save.bodyless, Some(true)); + + let find = s.definitions.iter().find(|d| d.name == "Repo.find").unwrap(); + assert_ne!(find.bodyless, Some(true)); + } + #[test] fn extracts_struct_fields() { let s = parse_rust("struct User { name: String, age: u32 }"); diff --git a/crates/codegraph-core/src/extractors/scala.rs b/crates/codegraph-core/src/extractors/scala.rs index 60c4fa34a..f06b10884 100644 --- a/crates/codegraph-core/src/extractors/scala.rs +++ b/crates/codegraph-core/src/extractors/scala.rs @@ -213,6 +213,7 @@ fn handle_scala_class_definition(node: &Node, source: &[u8], symbols: &mut FileS complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_scala_extends(node, source, &class_name, symbols); } @@ -232,6 +233,7 @@ fn handle_scala_trait_definition(node: &Node, source: &[u8], symbols: &mut FileS complexity: None, cfg: None, children: None, + bodyless: None, }); extract_scala_extends(node, source, &trait_name, symbols); } @@ -252,6 +254,7 @@ fn handle_scala_object_definition(node: &Node, source: &[u8], symbols: &mut File complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); extract_scala_extends(node, source, &obj_name, symbols); } @@ -278,6 +281,7 @@ fn handle_scala_function_definition(node: &Node, source: &[u8], symbols: &mut Fi complexity: compute_all_metrics(node, source, "scala"), cfg: build_function_cfg(node, "scala", source), children: opt_children(children), + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/solidity.rs b/crates/codegraph-core/src/extractors/solidity.rs index 6aa6e9694..d32de44c4 100644 --- a/crates/codegraph-core/src/extractors/solidity.rs +++ b/crates/codegraph-core/src/extractors/solidity.rs @@ -79,6 +79,7 @@ fn handle_contract_decl( complexity: None, cfg: None, children: opt_children(members), + bodyless: None, }); extract_inheritance(node, &name, source, symbols); @@ -128,6 +129,7 @@ fn extract_contract_member(child: &Node, source: &[u8]) -> Option { complexity: None, cfg: None, children: None, + bodyless: None, }) } "error_declaration" => { @@ -141,6 +143,7 @@ fn extract_contract_member(child: &Node, source: &[u8]) -> Option { complexity: None, cfg: None, children: None, + bodyless: None, }) } "modifier_definition" => { @@ -154,6 +157,7 @@ fn extract_contract_member(child: &Node, source: &[u8]) -> Option { complexity: None, cfg: None, children: None, + bodyless: None, }) } _ => None, @@ -231,6 +235,7 @@ fn handle_struct_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(members), + bodyless: None, }); } @@ -270,6 +275,7 @@ fn handle_enum_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(members), + bodyless: None, }); } @@ -296,6 +302,7 @@ fn handle_function_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(params), + bodyless: None, }); } @@ -318,6 +325,7 @@ fn handle_modifier_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -340,6 +348,7 @@ fn handle_event_def(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -362,6 +371,7 @@ fn handle_error_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -384,6 +394,7 @@ fn handle_state_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/swift.rs b/crates/codegraph-core/src/extractors/swift.rs index 51a57d0a0..25b9bb20e 100644 --- a/crates/codegraph-core/src/extractors/swift.rs +++ b/crates/codegraph-core/src/extractors/swift.rs @@ -243,6 +243,7 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } _ => { @@ -256,6 +257,7 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept complexity: None, cfg: None, children: opt_children(children), + bodyless: None, }); } } @@ -278,6 +280,7 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept complexity: None, cfg: None, children: None, + bodyless: None, }); // Protocol can also have inheritance extract_swift_inheritance(node, source, &proto_name, symbols); @@ -305,6 +308,7 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept complexity: compute_all_metrics(node, source, "swift"), cfg: build_function_cfg(node, "swift", source), children: opt_children(children), + bodyless: None, }); } } diff --git a/crates/codegraph-core/src/extractors/verilog.rs b/crates/codegraph-core/src/extractors/verilog.rs index 8d8e8204f..b50765c7f 100644 --- a/crates/codegraph-core/src/extractors/verilog.rs +++ b/crates/codegraph-core/src/extractors/verilog.rs @@ -69,6 +69,7 @@ fn handle_module_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: opt_children(ports), + bodyless: None, }); } @@ -86,6 +87,7 @@ fn handle_interface_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -103,6 +105,7 @@ fn handle_package_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -128,6 +131,7 @@ fn handle_class_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); if let Some(superclass) = find_class_superclass(node, source) { @@ -193,6 +197,7 @@ fn handle_function_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -215,6 +220,7 @@ fn handle_task_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/extractors/zig.rs b/crates/codegraph-core/src/extractors/zig.rs index ed412c914..6d491b5b0 100644 --- a/crates/codegraph-core/src/extractors/zig.rs +++ b/crates/codegraph-core/src/extractors/zig.rs @@ -50,6 +50,7 @@ fn handle_zig_function(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: compute_all_metrics(node, source, "zig"), cfg: build_function_cfg(node, "zig", source), children: opt_children(params), + bodyless: None, }); } @@ -98,6 +99,7 @@ fn handle_zig_variable(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } @@ -119,6 +121,7 @@ fn try_handle_zig_type_def(node: &Node, source: &[u8], symbols: &mut FileSymbols complexity: None, cfg: None, children, + bodyless: None, }); return true; } @@ -259,6 +262,7 @@ fn handle_zig_test(node: &Node, source: &[u8], symbols: &mut FileSymbols) { complexity: None, cfg: None, children: None, + bodyless: None, }); } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index bd1cfc04e..b22cf2e7c 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -99,6 +99,14 @@ pub struct Definition { pub cfg: Option, #[napi(ts_type = "Definition[] | undefined")] pub children: Option>, + /// Set when the underlying AST node structurally has no executable body + /// (an interface/protocol/trait method signature, an abstract method with + /// no block, a Rust trait `function_signature_item`, etc). Mirrors the TS + /// `Definition.bodyless` signal — see `hasFuncBody` in + /// `src/ast-analysis/apply-results.ts` (issue #1922). A dotted name alone + /// does not imply this: it's the normal qualified name for real bodied + /// class/struct/impl/module methods across every extractor. + pub bodyless: Option, } #[napi(object)] diff --git a/src/ast-analysis/apply-results.ts b/src/ast-analysis/apply-results.ts index 9e83fb202..09985a420 100644 --- a/src/ast-analysis/apply-results.ts +++ b/src/ast-analysis/apply-results.ts @@ -32,19 +32,29 @@ interface CfgFuncResult { cyclomatic?: number; } -/** Check if a definition has a real function body (not a type signature). */ +/** + * Check if a definition has a real function body (not a signature-only + * declaration). Relies on `bodyless`, a direct signal the extractor sets + * from the AST node's body field — not on name shape. A dotted name alone + * (`Class.method`) does not imply "no body": that's the normal qualified + * name for class/struct/impl methods and module-table functions (Lua's + * `M.foo`, Go/Java/C#/PHP/Rust receiver or impl methods) in every extractor, + * so filtering on it excluded real, bodied functions and could make an + * entire file's `defs.some(...)` gate false when every function in it + * happened to have a dotted name (issue #1922). + */ export function hasFuncBody(d: { - name: string; kind: string; line: number; endLine?: number | null; + bodyless?: boolean; }): boolean { return ( (d.kind === 'function' || d.kind === 'method') && d.line > 0 && d.endLine != null && d.endLine > d.line && - !d.name.includes('.') + !d.bodyless ); } diff --git a/src/extractors/csharp.ts b/src/extractors/csharp.ts index 8a3a06e9e..21ffb7d9c 100644 --- a/src/extractors/csharp.ts +++ b/src/extractors/csharp.ts @@ -143,6 +143,7 @@ function handleCsInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void kind: 'method', line: child.startPosition.row + 1, endLine: child.endPosition.row + 1, + bodyless: !child.childForFieldName('body'), }); } } @@ -178,6 +179,7 @@ function handleCsMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { endLine: nodeEndLine(node), children: params.length > 0 ? params : undefined, visibility: extractModifierVisibility(node), + bodyless: !node.childForFieldName('body'), }); } diff --git a/src/extractors/go.ts b/src/extractors/go.ts index 21549134a..0a61a0325 100644 --- a/src/extractors/go.ts +++ b/src/extractors/go.ts @@ -92,6 +92,7 @@ function handleGoMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { endLine: nodeEndLine(node), children: params.length > 0 ? params : undefined, visibility: goVisibility(nameNode.text), + bodyless: !node.childForFieldName('body'), }); } @@ -171,6 +172,7 @@ function handleGoInterfaceType( kind: 'method', line: member.startPosition.row + 1, endLine: member.endPosition.row + 1, + bodyless: !member.childForFieldName('body'), }); } } diff --git a/src/extractors/java.ts b/src/extractors/java.ts index 2c60f8016..58be50b04 100644 --- a/src/extractors/java.ts +++ b/src/extractors/java.ts @@ -190,6 +190,7 @@ function extractJavaInterfaceMethods( kind: 'method', line: nodeStartLine(child), endLine: nodeEndLine(child), + bodyless: !child.childForFieldName('body'), }); } } @@ -224,6 +225,7 @@ function handleJavaMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void endLine: nodeEndLine(node), children: params.length > 0 ? params : undefined, visibility: extractModifierVisibility(node), + bodyless: !node.childForFieldName('body'), }); } diff --git a/src/extractors/javascript.ts b/src/extractors/javascript.ts index 1b9be8ccb..0e4cf54bb 100644 --- a/src/extractors/javascript.ts +++ b/src/extractors/javascript.ts @@ -1949,6 +1949,7 @@ function extractInterfaceMethods( kind: child.type === 'method_signature' ? 'method' : 'property', line: nodeStartLine(child), endLine: nodeEndLine(child), + bodyless: !child.childForFieldName('body'), }); } } diff --git a/src/extractors/php.ts b/src/extractors/php.ts index b43c2489d..57227ee04 100644 --- a/src/extractors/php.ts +++ b/src/extractors/php.ts @@ -222,6 +222,7 @@ function handlePhpInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): voi kind: 'method', line: child.startPosition.row + 1, endLine: child.endPosition.row + 1, + bodyless: !child.childForFieldName('body'), }); } } @@ -268,6 +269,7 @@ function handlePhpMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { endLine: nodeEndLine(node), children: params.length > 0 ? params : undefined, visibility: extractModifierVisibility(node), + bodyless: !node.childForFieldName('body'), }); } diff --git a/src/extractors/rust.ts b/src/extractors/rust.ts index 490e5c2b6..10c7115c2 100644 --- a/src/extractors/rust.ts +++ b/src/extractors/rust.ts @@ -185,6 +185,7 @@ function extractTraitMethods(body: TreeSitterNode, traitName: string, ctx: Extra kind: 'method', line: child.startPosition.row + 1, endLine: child.endPosition.row + 1, + bodyless: !child.childForFieldName('body'), }); } } diff --git a/src/features/cfg.ts b/src/features/cfg.ts index 48f1a7c42..9e975ea8e 100644 --- a/src/features/cfg.ts +++ b/src/features/cfg.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { hasFuncBody } from '../ast-analysis/apply-results.js'; import { CFG_RULES } from '../ast-analysis/rules/index.js'; import { makeCfgRules as _makeCfgRules, @@ -97,14 +98,7 @@ interface FileSymbols { */ function hasNativeCfgForFile(symbols: FileSymbols): boolean { return symbols.definitions - .filter( - (d) => - (d.kind === 'function' || d.kind === 'method') && - d.line > 0 && - d.endLine != null && - d.endLine > d.line && - !d.name.includes('.'), - ) + .filter((d) => hasFuncBody(d)) .every((d) => d.cfg === null || (d.cfg?.blocks?.length ?? 0) > 0); } diff --git a/src/features/complexity.ts b/src/features/complexity.ts index fb3cca381..7e4eaf2d3 100644 --- a/src/features/complexity.ts +++ b/src/features/complexity.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import { hasFuncBody } from '../ast-analysis/apply-results.js'; import { computeLOCMetrics as _computeLOCMetrics, computeMaintainabilityIndex as _computeMaintainabilityIndex, @@ -389,6 +390,7 @@ interface FileSymbols { kind: string; line: number; endLine?: number; + bodyless?: boolean; complexity?: { cognitive: number; cyclomatic: number; @@ -407,18 +409,11 @@ async function initWasmParsersIfNeeded( if (!symbols._tree) { const ext = path.extname(relPath).toLowerCase(); if (!COMPLEXITY_EXTENSIONS.has(ext)) continue; - // Only consider definitions with real function bodies (non-dotted names, - // multi-line span). Interface/type property signatures are extracted as - // methods but correctly lack complexity data from the native engine. - const hasPrecomputed = symbols.definitions.every( - (d) => - (d.kind !== 'function' && d.kind !== 'method') || - d.complexity || - d.name.includes('.') || - !d.endLine || - d.endLine <= d.line, - ); - if (!hasPrecomputed) { + // Only consider definitions with real function bodies. Signature-only + // declarations (interface/abstract method stubs, marked `bodyless` by + // the extractor) correctly lack complexity data from the native engine. + const needsWasmComplexity = symbols.definitions.some((d) => hasFuncBody(d) && !d.complexity); + if (needsWasmComplexity) { const { createParsers } = await import('../domain/parser.js'); const parsers = await createParsers(); const extToLang = buildExtToLangMap(); @@ -552,11 +547,12 @@ function classifyDefinitionForNativeBulk( if (def.kind !== 'function' && def.kind !== 'method') return 'skip'; if (!def.line) return 'skip'; if (!def.complexity) { - // Interface/type property signatures and single-line stubs are extracted - // as methods but the native engine correctly never assigns complexity. + // Signature-only declarations (interface/abstract method stubs, marked + // `bodyless` by the extractor) and single-line stubs are extracted as + // methods but the native engine correctly never assigns complexity. // Mirror the leniency in initWasmParsersIfNeeded to avoid bailing out // of the native bulk-insert path for every TypeScript codebase (#846). - if (def.name.includes('.') || !def.endLine || def.endLine <= def.line) return 'skip'; + if (!hasFuncBody(def)) return 'skip'; // Languages without complexity rules will never have data — skip them // rather than bailing out of the entire native bulk path. if (!langSupported) return 'skip'; diff --git a/src/types.ts b/src/types.ts index 533a577ec..867040f8d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -429,6 +429,16 @@ export interface Definition { children?: SubDeclaration[]; visibility?: 'public' | 'private' | 'protected'; decorators?: string[]; + /** + * Set by the extractor when the underlying AST node structurally has no + * executable body (e.g. a TS `method_signature`, a Go interface + * `method_elem`, an abstract/interface method with no block, a Rust trait + * `function_signature_item`). This is a direct signal from the grammar — + * not inferred from name shape — so it stays correct for dotted names that + * denote real bodied methods (Lua's `M.foo`, Go/Java/C#/PHP/Rust receiver + * or impl methods, any `Class.method` qualified name). + */ + bodyless?: boolean; /** Populated post-analysis by the complexity visitor. */ complexity?: DefinitionComplexity; /** Populated post-analysis by the CFG visitor. */ diff --git a/tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts b/tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts new file mode 100644 index 000000000..3a5d472e9 --- /dev/null +++ b/tests/integration/issue-1922-wasm-complexity-dotted-names.test.ts @@ -0,0 +1,175 @@ +/** + * Integration test for #1922: WASM complexity engine skips an entire file when + * every function/method in it has a dotted name. + * + * `hasFuncBody()` used to treat any dotted name (`Class.method`, `M.foo`) as a + * "type-signature stub, not a real body" — a proxy that happened to be true for + * the one case that motivated it (TS interface `method_signature`) but false for + * every other dotted-name producer: Lua's `M.foo` module-table pattern, Go/Java/ + * C#/PHP/Rust receiver or impl methods, and even a plain `Class.method` in any + * language. When *every* function/method definition in a file had a dotted name, + * the file-level "does this file need WASM complexity" gate (`defs.some(...)`) + * went false for the whole file, and none of its functions got complexity data. + * + * The fix replaces the name-shape heuristic with `Definition.bodyless`, a direct + * signal the extractor sets from the AST node's body field. This test builds + * fixtures reproducing the exact reported patterns (Lua module table, Go + * receiver-only file) plus a mixed TS file (real dotted method + a genuinely + * bodyless multi-line interface signature) to guard against regressing the + * original interface-stub-filtering behavior the heuristic was protecting. + */ + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import Database from 'better-sqlite3'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { buildGraph } from '../../src/domain/graph/builder.js'; + +const FIXTURE = { + // Lua module-table pattern: every function has a dotted name (M.foo), and every + // one has a real, multi-line body. + 'repo.lua': ` +local M = {} + +function M.save(id, value) + if value < 0 then + return false + end + for i = 1, 3 do + if i == value then + return true + end + end + return true +end + +function M.find(id) + if id == nil then + return nil + end + return id +end + +return M +`, + // Go receiver-only file: no free functions, only dotted receiver methods — + // each with a real, multi-line body. + 'repo.go': ` +package main + +type Repo struct { + items map[string]int +} + +func (r *Repo) Save(id string, value int) bool { + if value < 0 { + return false + } + for i := 0; i < 3; i++ { + if i == value { + r.items[id] = value + } + } + return true +} + +func (r *Repo) Find(id string) (int, bool) { + v, ok := r.items[id] + if !ok { + return 0, false + } + return v, true +} +`, + // Mixed TS file: a real class method (dotted, bodied) alongside a genuinely + // bodyless interface method signature that spans multiple lines (the exact + // shape the original dot-check was added to exclude, per #606). + 'repo.ts': ` +interface Repository { + save( + id: string, + value: number, + ): boolean; +} + +class InMemoryRepository implements Repository { + private items: Map = new Map(); + + save(id: string, value: number): boolean { + if (value < 0) { + return false; + } + for (let i = 0; i < 3; i++) { + if (i === value) { + this.items.set(id, value); + } + } + return true; + } +} +`, +}; + +let tmpDir: string; +let dbPath: string; + +beforeAll(async () => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1922-')); + for (const [rel, content] of Object.entries(FIXTURE)) { + fs.writeFileSync(path.join(tmpDir, rel), content); + } + await buildGraph(tmpDir, { engine: 'wasm', incremental: false, skipRegistry: true }); + dbPath = path.join(tmpDir, '.codegraph', 'graph.db'); +}); + +afterAll(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +function complexityRows(file: string): Array<{ name: string; cyclomatic: number }> { + const db = new Database(dbPath, { readonly: true }); + try { + return db + .prepare( + `SELECT n.name AS name, fc.cyclomatic AS cyclomatic + FROM function_complexity fc + JOIN nodes n ON n.id = fc.node_id + WHERE n.file = ? + ORDER BY n.name`, + ) + .all(file) as Array<{ name: string; cyclomatic: number }>; + } finally { + db.close(); + } +} + +describe('issue #1922: WASM complexity file-level gate with all-dotted-name files', () => { + it('computes complexity for every function in a Lua module-table file (M.foo pattern)', () => { + const rows = complexityRows('repo.lua'); + expect(rows.map((r) => r.name).sort()).toEqual(['M.find', 'M.save']); + const save = rows.find((r) => r.name === 'M.save'); + // save() has 2 branches (if value<0, if i==value) — cyclomatic must reflect + // the real body, not a trivial/absent value. + expect(save?.cyclomatic).toBeGreaterThan(1); + }); + + it('computes complexity for every method in a Go receiver-only file (no free functions)', () => { + const rows = complexityRows('repo.go'); + expect(rows.map((r) => r.name).sort()).toEqual(['Repo.Find', 'Repo.Save']); + const save = rows.find((r) => r.name === 'Repo.Save'); + expect(save?.cyclomatic).toBeGreaterThan(1); + }); + + it('computes complexity for a real dotted class method while still excluding the bodyless interface stub', () => { + const rows = complexityRows('repo.ts'); + const names = rows.map((r) => r.name); + // The real, bodied class method must be present with real complexity. + expect(names).toContain('InMemoryRepository.save'); + const save = rows.find((r) => r.name === 'InMemoryRepository.save'); + expect(save?.cyclomatic).toBeGreaterThan(1); + // The interface's multi-line, bodyless method signature must NOT get a + // spurious complexity entry (regression guard for the original #606 intent). + expect(names).not.toContain('Repository.save'); + }); +}); diff --git a/tests/unit/apply-results.test.ts b/tests/unit/apply-results.test.ts index ca8ce446f..3826d973e 100644 --- a/tests/unit/apply-results.test.ts +++ b/tests/unit/apply-results.test.ts @@ -52,8 +52,25 @@ describe('hasFuncBody', () => { expect(hasFuncBody({ name: 'foo', kind: 'function', line: 5, endLine: 5 })).toBe(false); }); - it('is false for dotted names (e.g. synthetic Class.member defs)', () => { - expect(hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10 })).toBe(false); + it('is true for a dotted name with a real body (Class.method, module-table function, receiver method) — issue #1922', () => { + // A dotted name alone must never disqualify a real, bodied function: it's the normal + // qualified name for class/struct/impl methods (`Class.method`) and module-table + // functions (Lua's `M.foo`, Go/Java/C#/PHP/Rust receiver or impl methods) across every + // extractor. Regression guard for the bug where the file-level "does this file need + // complexity" gate (`defs.some(hasFuncBody)`) went false for an entire file when every + // function in it happened to have a dotted name. + expect(hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10 })).toBe(true); + expect(hasFuncBody({ name: 'M.foo', kind: 'method', line: 5, endLine: 10 })).toBe(true); + }); + + it('is false when the extractor marks the definition bodyless (interface/trait/abstract signature)', () => { + expect( + hasFuncBody({ name: 'Foo.bar', kind: 'method', line: 5, endLine: 10, bodyless: true }), + ).toBe(false); + // Even a non-dotted signature-only declaration is excluded via `bodyless`. + expect( + hasFuncBody({ name: 'bar', kind: 'function', line: 5, endLine: 10, bodyless: true }), + ).toBe(false); }); });