mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
Merge pull request #2685 from DeusData/fix/rust-generic-impl-call-scope
fix(rust): generic impl methods source their own calls (#763 Rust share)
This commit is contained in:
@@ -1176,6 +1176,20 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkS
|
||||
if (!name || !name[0]) {
|
||||
return NULL;
|
||||
}
|
||||
/* Rust: the impl's type text carries its generic arguments (`impl<T> Gen<T>`,
|
||||
* `impl<'a> Parser<'a>`), which the def side strips before naming the type
|
||||
* (extract_defs.c extract_rust_impl: `Buffer<T>` -> `Buffer`). Strip them the
|
||||
* same way, or every method of a generic impl gets a scope QN no node carries
|
||||
* and all its calls fall back to the File node. */
|
||||
if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) {
|
||||
char *lt = strchr(name, '<');
|
||||
if (lt == name) {
|
||||
return NULL; /* `<T as Tr>`-style self type: no name to scope by */
|
||||
}
|
||||
if (lt) {
|
||||
*lt = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Nested class: prefix with the enclosing class QN (Outer.Inner) so this
|
||||
* scope QN matches the def-side class QN (extract_defs.c compute_class_qn /
|
||||
|
||||
@@ -306,6 +306,22 @@ static void rust_strip_turbofish(char *path) {
|
||||
*write = '\0';
|
||||
}
|
||||
|
||||
/* In place: `Gen<T>` -> `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) {
|
||||
@@ -5274,6 +5290,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<T: Trait> ForeignTrait for T { ... }`
|
||||
* where type_text is a name that appears in the impl's type
|
||||
@@ -6028,6 +6045,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);
|
||||
|
||||
@@ -7178,6 +7178,63 @@ TEST(extract_perl_method_call_flags_is_method) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The def QN of `name` and the enclosing-function QN of the call `callee` inside
|
||||
* it, from one extraction result. */
|
||||
static void rust_def_and_scope(const CBMFileResult *r, const char *name, const char *callee,
|
||||
const char **def_qn, const char **scope_qn) {
|
||||
*def_qn = NULL;
|
||||
*scope_qn = NULL;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *d = &r->defs.items[i];
|
||||
if (d->name && d->qualified_name && strcmp(d->name, name) == 0) {
|
||||
*def_qn = d->qualified_name;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < r->calls.count; i++) {
|
||||
const CBMCall *c = &r->calls.items[i];
|
||||
if (c->callee_name && strcmp(c->callee_name, callee) == 0 && c->enclosing_func_qn &&
|
||||
strstr(c->enclosing_func_qn, name)) {
|
||||
*scope_qn = c->enclosing_func_qn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* F-R4: a method of a generic impl (`impl<T> Gen<T>`, `impl<'a> Life<'a>`) got a
|
||||
* call scope that kept the generic arguments ("...Gen<T>.in_generic") while the
|
||||
* def side strips them ("...Gen.in_generic"), so no node carried the scope and
|
||||
* every call in the body fell back to the File node (base census: 84,331 Rust
|
||||
* CALLS sourced from a File although inside a function, 96% of all File-sourced
|
||||
* Rust CALLS). The def QN and the call scope must agree. */
|
||||
TEST(extract_rust_generic_impl_call_scope_matches_def) {
|
||||
CBMFileResult *r = extract("pub struct Plain;\n"
|
||||
"impl Plain {\n"
|
||||
" pub fn in_plain(&self) -> u32 { helper() }\n"
|
||||
"}\n"
|
||||
"pub struct Gen<T> { v: T }\n"
|
||||
"impl<T> Gen<T> {\n"
|
||||
" pub fn in_generic(&self) -> u32 { helper() }\n"
|
||||
"}\n"
|
||||
"pub struct Life<'a> { s: &'a str }\n"
|
||||
"impl<'a> Life<'a> {\n"
|
||||
" pub fn in_lifetime(&self) -> u32 { helper() }\n"
|
||||
"}\n"
|
||||
"pub fn helper() -> u32 { 1 }\n",
|
||||
CBM_LANG_RUST, "t", "src/lib.rs");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
static const char *const methods[] = {"in_plain", "in_generic", "in_lifetime", NULL};
|
||||
for (int m = 0; methods[m]; m++) {
|
||||
const char *def_qn = NULL;
|
||||
const char *scope_qn = NULL;
|
||||
rust_def_and_scope(r, methods[m], "helper", &def_qn, &scope_qn);
|
||||
ASSERT_NOT_NULL(def_qn);
|
||||
ASSERT_NOT_NULL(scope_qn);
|
||||
ASSERT_STR_EQ(scope_qn, def_qn);
|
||||
}
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Languages OUTSIDE the is_method flag set (only Perl and TS/JS/TSX set it) must
|
||||
* be unaffected: a Go method call never sets is_method. */
|
||||
TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
|
||||
@@ -12512,6 +12569,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_perl_config_string_not_a_callee);
|
||||
RUN_TEST(extract_perl_builtin_call_is_function_not_method);
|
||||
RUN_TEST(extract_perl_method_call_flags_is_method);
|
||||
RUN_TEST(extract_rust_generic_impl_call_scope_matches_def);
|
||||
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
|
||||
RUN_TEST(extract_python_member_call_flags_is_method);
|
||||
RUN_TEST(extract_python_bare_call_flags_locally_bound_callee);
|
||||
|
||||
@@ -9042,6 +9042,94 @@ TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel) {
|
||||
return rust_assoc_call_free_namesake_case(52);
|
||||
}
|
||||
|
||||
/* F-R4 (#763, Rust): calls inside a method of a generic impl (`impl<T> Gen<T>`,
|
||||
* `impl<'a> Life<'a>`) were sourced from the File node: the call scope kept the
|
||||
* generic arguments the def side strips. Each method must be the source of its
|
||||
* own call. pad_files > 0 forces the parallel resolver. */
|
||||
static int rust_generic_impl_attribution_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_gimpl_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
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<T> { v: T }\n"
|
||||
"impl<T> Gen<T> {\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];
|
||||
snprintf(name, sizeof(name), "src/filler%d.rs", i);
|
||||
snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
|
||||
char *saved = NULL;
|
||||
if (pad_files > 0) {
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/rust_gimpl.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "in_generic", "helper_a"));
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "in_lifetime", "helper_b"));
|
||||
/* ... 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);
|
||||
if (pad_files > 0) {
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_generic_impl_method_sources_its_calls) {
|
||||
return rust_generic_impl_attribution_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_generic_impl_method_sources_its_calls_parallel) {
|
||||
return rust_generic_impl_attribution_case(52);
|
||||
}
|
||||
|
||||
TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
|
||||
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
|
||||
* second, independent resolver in pass_parallel.c (resolve_file_usages /
|
||||
@@ -24748,6 +24836,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language_parallel);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake);
|
||||
RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel);
|
||||
RUN_TEST(pipeline_rust_generic_impl_method_sources_its_calls);
|
||||
RUN_TEST(pipeline_rust_generic_impl_method_sources_its_calls_parallel);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method);
|
||||
RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
|
||||
Reference in New Issue
Block a user