From f75af00a8b01db44d84fdeebaf493f400662d74d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 03:03:38 +0200 Subject: [PATCH] fix(rust-lsp): a generic impl's self type drops its type arguments The extractor fix above names a generic impl's methods `Gen.in_generic` on both the def and the call-scope side. The Rust LSP still built its self type from the impl's raw text, `Gen`. That broke three things: - its caller QN (`Gen.in_generic`) no longer joined the pipeline's call scope, so every LSP row inside a generic or lifetime impl was dropped for a registry guess. On 9 Rust repos that turned 1,927 lsp_* edges into suffix_match/unique_name; - `self.` and `Self::` dispatch never found the impl's own methods; `Self::assoc_g()` had no edge at all, on main as well; - the return-type harvest never matched those methods, so chained calls on them stayed untyped. Strip the generic arguments from the impl's self type in both places. Text that starts with `<` (a qualified path) is left alone. Signed-off-by: Martin Vogel --- internal/cbm/lsp/rust_lsp.c | 18 ++++++++++++++++++ tests/test_pipeline.c | 37 ++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/internal/cbm/lsp/rust_lsp.c b/internal/cbm/lsp/rust_lsp.c index 44680cb69..fda3111de 100644 --- a/internal/cbm/lsp/rust_lsp.c +++ b/internal/cbm/lsp/rust_lsp.c @@ -306,6 +306,22 @@ static void rust_strip_turbofish(char *path) { *write = '\0'; } +/* In place: `Gen` -> `Gen`, `foo::Bar<'a, T>` -> `foo::Bar` (#763). An + * impl's self type keeps its generic arguments in the source text, but every + * def-side QN (a method's receiver, the extractor's call scope) strips them. + * Unstripped, the LSP's caller QN, `self`/`Self` dispatch and the return-type + * harvest never matched a generic impl's methods. Text starting with `<` (a + * qualified path) is left alone. Safe on NULL. */ +static void rust_strip_impl_type_args(char *type_text) { + if (!type_text) { + return; + } + char *lt = strchr(type_text, '<'); + if (lt && lt > type_text) { + *lt = '\0'; + } +} + /* Look up a `use` alias and return its fully-qualified module path, * or NULL if absent. The returned pointer aliases into the use map. */ static const char *rust_resolve_use(RustLSPContext *ctx, const char *local_name) { @@ -5245,6 +5261,7 @@ static void rust_process_impl(RustLSPContext *ctx, TSNode impl_node) { char *type_text = rust_node_text(ctx, type_node); if (!type_text) return; + rust_strip_impl_type_args(type_text); /* Detect blanket impl: `impl ForeignTrait for T { ... }` * where type_text is a name that appears in the impl's type @@ -5999,6 +6016,7 @@ void cbm_rust_build_local_registry(CBMArena *arena, CBMTypeRegistry *reg, CBMFil if (ts_node_is_null(type_node) || ts_node_is_null(body)) continue; char *type_name = cbm_node_text(arena, type_node, source); + rust_strip_impl_type_args(type_name); if (!type_name || !type_name[0]) continue; const char *type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 0111318a2..085fa55dd 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -8879,17 +8879,21 @@ static int rust_generic_impl_attribution_case(int pad_files) { FAIL("tmpdir"); } write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"fx\"\nversion = \"0.1.0\"\n"); - write_temp_file(tmp, "src/lib.rs", - "pub struct Gen { v: T }\n" - "impl Gen {\n" - " pub fn in_generic(&self) -> u32 { helper_a() }\n" - "}\n" - "pub struct Life<'a> { s: &'a str }\n" - "impl<'a> Life<'a> {\n" - " pub fn in_lifetime(&self) -> u32 { helper_b() }\n" - "}\n" - "pub fn helper_a() -> u32 { 1 }\n" - "pub fn helper_b() -> u32 { 2 }\n"); + write_temp_file( + tmp, "src/lib.rs", + "pub struct Gen { v: T }\n" + "impl Gen {\n" + " pub fn in_generic(&self) -> u32 { helper_a() + self.other_g() + Self::assoc_g() }\n" + " pub fn other_g(&self) -> u32 { 3 }\n" + " pub fn assoc_g() -> u32 { 4 }\n" + "}\n" + "pub struct Life<'a> { s: &'a str }\n" + "impl<'a> Life<'a> {\n" + " pub fn in_lifetime(&self) -> u32 { helper_b() + self.other_l() }\n" + " pub fn other_l(&self) -> u32 { 5 }\n" + "}\n" + "pub fn helper_a() -> u32 { 1 }\n" + "pub fn helper_b() -> u32 { 2 }\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; @@ -8918,6 +8922,17 @@ static int rust_generic_impl_attribution_case(int pad_files) { /* ... and not from the file. */ ASSERT_FALSE(cross_file_call_exists(s, project, "lib.rs", "helper_a")); ASSERT_FALSE(cross_file_call_exists(s, project, "lib.rs", "helper_b")); + /* The LSP side names the impl's self type without its arguments too: its + * rows join these callers (lsp_*, not a registry fallback), and `self.` / + * `Self::` dispatch inside a generic impl resolves (Self::assoc_g had no + * edge at all). */ + ASSERT_TRUE( + cross_file_call_has_strategy(s, project, "in_generic", "helper_a", "\"strategy\":\"lsp_")); + ASSERT_TRUE( + cross_file_call_has_strategy(s, project, "in_generic", "other_g", "\"strategy\":\"lsp_")); + ASSERT_TRUE(cross_file_call_exists(s, project, "in_generic", "assoc_g")); + ASSERT_TRUE( + cross_file_call_has_strategy(s, project, "in_lifetime", "other_l", "\"strategy\":\"lsp_")); cbm_store_close(s); cbm_pipeline_free(p);