mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 08:35:21 +08:00
Merge pull request #1049 from DeusData/fix/988-py-alias-import
fix(py-lsp): resolve calls through aliased imports
This commit is contained in:
@@ -206,8 +206,19 @@ void py_lsp_bind_imports(PyLSPContext *ctx) {
|
||||
// resolution checks the registry to upgrade to MODULE / class
|
||||
// / function as appropriate.
|
||||
t = cbm_type_named(ctx->arena, qn);
|
||||
} else if (strchr(qn, '.') != NULL) {
|
||||
// Dotted path whose tail does NOT match the local name: an
|
||||
// ALIASED binding — `from X import Y as Z` (Z names function/
|
||||
// class X.Y) or `import a.b as z` (z names module a.b). The
|
||||
// CBMImport shape cannot distinguish the two, but NAMED(qn)
|
||||
// covers both: phase 6 upgrades it to the registered function/
|
||||
// class for the from-import and to MODULE for the module alias.
|
||||
// Binding MODULE here made `g()` calls on `from m import f as g`
|
||||
// resolve as calls on a module — lsp=MISS, and the whole CALLS
|
||||
// edge was lost (#988).
|
||||
t = cbm_type_named(ctx->arena, qn);
|
||||
} else {
|
||||
// `import X` / `import X as Y` — bind to MODULE(X).
|
||||
// `import X` / `import X as Y` (single segment) — MODULE(X).
|
||||
t = cbm_type_module(ctx->arena, qn);
|
||||
}
|
||||
py_scope_bind(ctx, local, t);
|
||||
@@ -1910,11 +1921,23 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
|
||||
py_scope_restore(ctx, saved);
|
||||
return;
|
||||
}
|
||||
// Constructor call (ClassName())
|
||||
// Constructor call (ClassName()) — or a call through any other
|
||||
// NAMED scope binding (aliased imports land here too).
|
||||
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
|
||||
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
|
||||
const char *qn = in_scope->data.named.qualified_name;
|
||||
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f);
|
||||
const char *tail = strrchr(qn, '.');
|
||||
const char *qn_short = tail ? tail + 1 : qn;
|
||||
if (strcmp(qn_short, fname) != 0) {
|
||||
/* Aliased binding (`from m import f as g; g()`): the textual
|
||||
* callee ("g") differs from the resolved QN's tail ("f"), so
|
||||
* the pass join would never match them. Stash the textual
|
||||
* name in `reason` under a join-gated strategy, mirroring
|
||||
* lsp_dict_dispatch (see lsp_resolve.h) (#988). */
|
||||
py_emit_resolved_call_reason(ctx, qn, "lsp_import_alias", 0.85f, fname);
|
||||
} else {
|
||||
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Module-local function
|
||||
|
||||
@@ -174,6 +174,7 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
|
||||
strcmp(rc->strategy, "lsp_method_ref_ctor") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_method_ref_ctor_synth") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_dict_dispatch") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_import_alias") == 0 ||
|
||||
strcmp(rc->strategy, "lsp_destructor") == 0 ||
|
||||
strcmp(rc->strategy, "php_method_dynamic") == 0) &&
|
||||
strcmp(cbm_lsp_bare_segment(rc->reason), call_short) == 0)) {
|
||||
|
||||
@@ -1207,6 +1207,41 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* True if a CALLS edge exists whose source QN ends with `src_suffix` and
|
||||
* target QN ends with `tgt_suffix`. */
|
||||
static int calls_edge_between(cbm_store_t *store, const char *project, const char *src_suffix,
|
||||
const char *tgt_suffix) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) != CBM_STORE_OK)
|
||||
return 0;
|
||||
int found = 0;
|
||||
size_t ssl = strlen(src_suffix);
|
||||
size_t tsl = strlen(tgt_suffix);
|
||||
for (int i = 0; i < n && !found; i++) {
|
||||
cbm_node_t s, t;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].source_id, &s) != CBM_STORE_OK)
|
||||
continue;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &t) != CBM_STORE_OK) {
|
||||
cbm_node_free_fields(&s);
|
||||
continue;
|
||||
}
|
||||
const char *sq = s.qualified_name;
|
||||
const char *tq = t.qualified_name;
|
||||
if (sq && tq) {
|
||||
size_t sql = strlen(sq);
|
||||
size_t tql = strlen(tq);
|
||||
if (sql >= ssl && strcmp(sq + sql - ssl, src_suffix) == 0 && tql >= tsl &&
|
||||
strcmp(tq + tql - tsl, tgt_suffix) == 0)
|
||||
found = 1;
|
||||
}
|
||||
cbm_node_free_fields(&s);
|
||||
cbm_node_free_fields(&t);
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* #999: URLs in CI/tooling configs (.pre-commit-config.yaml, .github/
|
||||
* workflows) are repository references for TOOLING, not endpoints this
|
||||
* service exposes. They must not mint __route__infra__ Route nodes — the
|
||||
@@ -1327,6 +1362,55 @@ static int calls_edge_targets(cbm_store_t *store, const char *project, const cha
|
||||
return found;
|
||||
}
|
||||
|
||||
/* #988: `from m import f as g; g()` produced NO CALLS edge — the Python LSP
|
||||
* bound the alias as MODULE("m.f") (the from-style heuristic keys on the
|
||||
* local name matching the module-path tail, which an alias never does), so
|
||||
* the call missed, and the registry fallback did not cover the minimal
|
||||
* two-file shape either. The alias must resolve exactly like the plain
|
||||
* import. The other import forms are pinned alongside as invariance guards
|
||||
* (all resolve on main today and must keep resolving). */
|
||||
TEST(contract_edge_python_aliased_import_call_resolves_issue988) {
|
||||
LangProj lp;
|
||||
static const LangFile f[] = {
|
||||
{"m.py", "def f(x):\n return x + 1\n"},
|
||||
{"pkg/__init__.py", ""},
|
||||
{"pkg/dm.py", "def h(x):\n return x\n"},
|
||||
{"caller_alias.py", "from m import f as g\n"
|
||||
"\n"
|
||||
"def use_alias(x):\n"
|
||||
" return g(x)\n"},
|
||||
{"caller_plain.py", "from m import f\n"
|
||||
"\n"
|
||||
"def use_plain(x):\n"
|
||||
" return f(x)\n"},
|
||||
{"caller_modalias.py", "import m as mm\n"
|
||||
"\n"
|
||||
"def use_modalias(x):\n"
|
||||
" return mm.f(x)\n"},
|
||||
{"caller_dotalias.py", "import pkg.dm as dz\n"
|
||||
"\n"
|
||||
"def use_dotalias(x):\n"
|
||||
" return dz.h(x)\n"}};
|
||||
cbm_store_t *store = lang_index_files(&lp, f, 7);
|
||||
ASSERT_TRUE(store != NULL);
|
||||
int alias = calls_edge_between(store, lp.project, ".use_alias", ".m.f");
|
||||
int plain = calls_edge_between(store, lp.project, ".use_plain", ".m.f");
|
||||
int modalias = calls_edge_between(store, lp.project, ".use_modalias", ".m.f");
|
||||
int dotalias = calls_edge_between(store, lp.project, ".use_dotalias", ".pkg.dm.h");
|
||||
if (!alias || !plain || !modalias || !dotalias) {
|
||||
fprintf(stderr,
|
||||
" [988] FAIL alias=%d plain=%d modalias=%d dotalias=%d (all import forms "
|
||||
"must produce the CALLS edge)\n",
|
||||
alias, plain, modalias, dotalias);
|
||||
}
|
||||
ASSERT_TRUE(alias); /* the #988 bug: aliased from-import call lost */
|
||||
ASSERT_TRUE(plain);
|
||||
ASSERT_TRUE(modalias);
|
||||
ASSERT_TRUE(dotalias);
|
||||
lang_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #871: a CommonJS require() binding shadowed call resolution. `const doThing
|
||||
* = require("../bs/doThing")` emitted a Variable def for `doThing` in the
|
||||
* importing module, so the call `doThing(...)` resolved to that same-module
|
||||
@@ -1572,6 +1656,7 @@ SUITE(lang_contract) {
|
||||
RUN_TEST(contract_edge_workspaces_imports_issue408);
|
||||
RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767);
|
||||
RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767);
|
||||
RUN_TEST(contract_edge_python_aliased_import_call_resolves_issue988);
|
||||
RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999);
|
||||
RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted);
|
||||
RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871);
|
||||
|
||||
+6
-10
@@ -192,16 +192,12 @@ TEST(pylsp_import_from_aliased) {
|
||||
const char *qns[] = {"pathlib.Path"};
|
||||
bind_imports_into_ctx(&ctx, &a, ®, locals, qns, 1);
|
||||
const CBMType *t = py_lsp_lookup_in_scope(&ctx, "P");
|
||||
/* Aliased name doesn't end module_qn with .P, so this binds as MODULE.
|
||||
* Phase 6 registry lookup will downgrade to NAMED if registry has no
|
||||
* matching module entry. Both behaviors are acceptable for v1; the
|
||||
* test asserts the entry exists with the correct QN. */
|
||||
ASSERT(t->kind == CBM_TYPE_NAMED || t->kind == CBM_TYPE_MODULE);
|
||||
if (t->kind == CBM_TYPE_NAMED) {
|
||||
ASSERT_STR_EQ(t->data.named.qualified_name, "pathlib.Path");
|
||||
} else {
|
||||
ASSERT_STR_EQ(t->data.module.module_qn, "pathlib.Path");
|
||||
}
|
||||
/* #988: an aliased from-import MUST bind NAMED (phase 6 upgrades it to
|
||||
* the registered function/class/module). The earlier MODULE binding made
|
||||
* `g()` calls on `from m import f as g` resolve as calls on a module —
|
||||
* lsp=MISS and the CALLS edge was lost. */
|
||||
ASSERT_EQ(t->kind, CBM_TYPE_NAMED);
|
||||
ASSERT_STR_EQ(t->data.named.qualified_name, "pathlib.Path");
|
||||
cbm_arena_destroy(&a);
|
||||
PASS();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user