mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(rust-lsp): a type-qualified call never takes the free-function fallback
`Opts::write(true)` is an associated function. When the Rust LSP cannot place it, the registry can (import_map or qualified_suffix onto fsx.Opts.write). The two free-function short-name fallbacks (the cross-crate member lookup and the project-wide lsp_short_name_unique) ran first, though. They bound the call to the project's only free `write`, and that LSP row pre-empted the right answer. While the Rust registry also held other languages' definitions, a same-named JS or Perl function often made the free name ambiguous and hid this. With Rust-only registries the hijack replaced 516 qualified_suffix and 84 import_map edges on 7 Rust repos. A call whose qualifying segment names a type now skips both fallbacks. That covers an ASCII-uppercase initial, `Self`, and a qualified `<T as Tr>::f` path. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -4128,6 +4128,32 @@ static bool rust_qn_has_suffix(const char *qualified_name, const char *suffix) {
|
||||
(qn_len == suffix_len || qualified_name[qn_len - suffix_len - 1U] == '.');
|
||||
}
|
||||
|
||||
/* True when the segment that qualifies a call path's tail names a TYPE:
|
||||
* `Store::new`, `Self::helper`, `T::default`, `<T as Tr>::f` (turbofish is
|
||||
* already stripped, so a `>` before the last `::` closes a qualified path).
|
||||
* Such a call is an associated function, never a free function, so the
|
||||
* free-function short-name fallbacks must not bind it: with a project-wide
|
||||
* unique free `new`/`write`, `Store::new()` landed on that function and the
|
||||
* LSP row pre-empted the registry's correct `Store.new`. Module segments are
|
||||
* snake_case, so an ASCII-uppercase initial is a type. `tail` must point just
|
||||
* past the path's last `::`, or equal `path` when there is none. */
|
||||
static bool rust_path_qualifier_is_type(const char *path, const char *tail) {
|
||||
if (!path || !tail || tail - path < 3) {
|
||||
return false;
|
||||
}
|
||||
const char *seg_end = tail - 2; /* the last "::" */
|
||||
if (seg_end[-1] == '>') {
|
||||
return true;
|
||||
}
|
||||
const char *seg = path;
|
||||
for (const char *p = path; p + 1 < seg_end; p++) {
|
||||
if (p[0] == ':' && p[1] == ':') {
|
||||
seg = p + 2;
|
||||
}
|
||||
}
|
||||
return seg < seg_end && seg[0] >= 'A' && seg[0] <= 'Z';
|
||||
}
|
||||
|
||||
/* Resolve only an exact identifier/scoped-path value. No branch, closure, or
|
||||
* other composite expression enters this helper. A suffix fallback is admitted
|
||||
* only for a qualified import path and only when it identifies one free func. */
|
||||
@@ -4514,6 +4540,9 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
} else {
|
||||
tail = path;
|
||||
}
|
||||
/* `Type::f` is an associated function: the two free-function
|
||||
* fallbacks below never bind it (see rust_path_qualifier_is_type). */
|
||||
const bool assoc_call = rust_path_qualifier_is_type(path, tail);
|
||||
|
||||
/* Cross-crate workspace-member resolution (#56): when the call
|
||||
* path's head is a declared Cargo workspace member (e.g.
|
||||
@@ -4524,7 +4553,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
* MEMBER crate by matching the registered QN's `.<member>.`
|
||||
* path segment plus the call tail. Requires a parsed manifest
|
||||
* (threaded through pass_lsp_cross.c); NULL manifest skips this. */
|
||||
if (ctx->cargo_manifest && tail && *tail) {
|
||||
if (!assoc_call && ctx->cargo_manifest && tail && *tail) {
|
||||
const char *head_sep = strstr(path, "::");
|
||||
if (head_sep && head_sep > path) {
|
||||
char *head = cbm_arena_strndup(ctx->arena, path, (size_t)(head_sep - path));
|
||||
@@ -4568,7 +4597,7 @@ static void rust_resolve_call_expression_inner(RustLSPContext *ctx, TSNode node)
|
||||
* foo; use foo::bar; bar()` a chance to resolve when the
|
||||
* intermediate module wasn't tracked through an explicit
|
||||
* use-map entry. */
|
||||
if (tail && *tail && ctx->module_qn) {
|
||||
if (!assoc_call && tail && *tail && ctx->module_qn) {
|
||||
/* Crate prefix is the first dotted segment of module_qn after
|
||||
* the project name, but for simplicity we just match on
|
||||
* "starts with first dot-segment". */
|
||||
|
||||
+106
-29
@@ -8874,6 +8874,40 @@ TEST(pipeline_go_rw_usage_never_cross_into_c) {
|
||||
* the generic-engine lane on its 12-language fixture). A Rust call never
|
||||
* targets another language's definition. pad_files > 0 forces the parallel
|
||||
* resolver, whose Rust registry is the shared all-defs build. */
|
||||
/* Pads a Rust fixture with pad_files filler modules (pad_files > 0 forces the
|
||||
* parallel resolver: CBM_WORKERS=4 for the run), runs the full pipeline into
|
||||
* <tmp>/<db_name> and opens the store. NULL store = the run failed. */
|
||||
static cbm_store_t *rust_fixture_run(const char *tmp, int pad_files, const char *db_name,
|
||||
cbm_pipeline_t **out_p) {
|
||||
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/%s", tmp, db_name);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
if (pad_files > 0) {
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
}
|
||||
*out_p = p;
|
||||
return rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
}
|
||||
|
||||
static int rust_registry_rust_only_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_only_XXXXXX");
|
||||
@@ -8917,28 +8951,10 @@ static int rust_registry_rust_only_case(int pad_files) {
|
||||
"}\n"
|
||||
"\n"
|
||||
"1;\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_only.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);
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_only.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* Never onto the Perl constructor ... */
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "new", "perlsrc/Lib.pm"), 0);
|
||||
@@ -8947,14 +8963,6 @@ static int rust_registry_rust_only_case(int pad_files) {
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -8967,6 +8975,73 @@ TEST(pipeline_rust_call_never_binds_another_language_parallel) {
|
||||
return rust_registry_rust_only_case(52);
|
||||
}
|
||||
|
||||
/* `Opts::write(true)` is an associated function. The LSP leaves it to the
|
||||
* registry (import_map -> fsx.Opts.write), but its free-function short-name
|
||||
* fallback bound it to the project's only free `write` (cache.write) first, and
|
||||
* that LSP row pre-empted the right answer. With the cross-language namesakes
|
||||
* gone (above), this hijack replaced 600+ correct qualified_suffix/import_map
|
||||
* edges on the Rust corpus. A type-qualified call never takes that fallback. */
|
||||
static int rust_assoc_call_free_namesake_case(int pad_files) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_assoc_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n");
|
||||
write_temp_file(tmp, "src/fsx.rs",
|
||||
"pub struct Opts {\n"
|
||||
" pub w: bool,\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"impl Opts {\n"
|
||||
" pub fn write(w: bool) -> Opts {\n"
|
||||
" Opts { w }\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/cache.rs",
|
||||
"pub fn write(p: &str) -> usize {\n"
|
||||
" p.len()\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "src/main.rs",
|
||||
"mod cache;\n"
|
||||
"mod fsx;\n"
|
||||
"\n"
|
||||
"use crate::fsx::Opts;\n"
|
||||
"\n"
|
||||
"fn run() -> bool {\n"
|
||||
" let o = Opts::write(true);\n"
|
||||
" o.w\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fn main() {\n"
|
||||
" run();\n"
|
||||
" cache::write(\"x\");\n"
|
||||
"}\n");
|
||||
cbm_pipeline_t *p = NULL;
|
||||
cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_assoc.db", &p);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
/* The associated function, never the free namesake ... */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/fsx.rs"), 1);
|
||||
ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/cache.rs"), 0);
|
||||
/* ... while the module-qualified free call keeps its target. */
|
||||
ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "main", "write", "src/cache.rs"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake) {
|
||||
return rust_assoc_call_free_namesake_case(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel) {
|
||||
return rust_assoc_call_free_namesake_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 /
|
||||
@@ -23829,6 +23904,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
|
||||
RUN_TEST(pipeline_rust_call_never_binds_another_language);
|
||||
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_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