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);