mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
Merge pull request #2696 from DeusData/fix/py-super-c3-mro
fix(python-lsp): resolve super() through the C3 method resolution order
This commit is contained in:
@@ -1229,6 +1229,138 @@ static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const cha
|
||||
return py_lookup_attribute_depth(ctx, type_qn, member_name, 0);
|
||||
}
|
||||
|
||||
/* ── super(): the C3 method resolution order ──────────────────────── */
|
||||
|
||||
/* Longest linearization followed. A longer one (or more bases than this) keeps
|
||||
* the base-by-base walk of the caller. */
|
||||
#define PY_C3_MAX 64
|
||||
|
||||
/* The linearizations computed during one super() lookup: every class of a
|
||||
* diamond is linearized once, however many paths reach it. */
|
||||
typedef struct {
|
||||
const char *qn[PY_C3_MAX];
|
||||
const char **mro[PY_C3_MAX];
|
||||
int len[PY_C3_MAX];
|
||||
int count;
|
||||
} py_c3_cache_t;
|
||||
|
||||
static bool py_c3_in_tail(const char *qn, const char *const *seq, int head, int len) {
|
||||
for (int k = head + 1; k < len; k++) {
|
||||
if (strcmp(seq[k], qn) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The C3 head to take next: the first sequence head that is in no sequence's
|
||||
* tail. NULL when every head is blocked (Python rejects such a class). */
|
||||
static const char *py_c3_pick(const char **const *seqs, const int *lens, const int *heads,
|
||||
int nseq) {
|
||||
for (int s = 0; s < nseq; s++) {
|
||||
if (heads[s] >= lens[s])
|
||||
continue;
|
||||
const char *cand = seqs[s][heads[s]];
|
||||
bool blocked = false;
|
||||
for (int t = 0; t < nseq && !blocked; t++) {
|
||||
blocked = heads[t] < lens[t] && py_c3_in_tail(cand, seqs[t], heads[t], lens[t]);
|
||||
}
|
||||
if (!blocked)
|
||||
return cand;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Python's method resolution order of `type_qn`: the class, then the C3 merge
|
||||
* of its bases' linearizations and of the base list. A common base therefore
|
||||
* follows every class deriving from it. Sets *out to an arena array and returns
|
||||
* its length, or -1 when there is no C3 order, the hierarchy is deeper than
|
||||
* CBM_LSP_MAX_LOOKUP_DEPTH, or the order is longer than PY_C3_MAX. A base the
|
||||
* registry does not know (external, unresolved) linearizes to itself. */
|
||||
static int py_c3_linearize(PyLSPContext *ctx, py_c3_cache_t *cache, const char *type_qn,
|
||||
const char ***out, int depth) {
|
||||
for (int c = 0; c < cache->count; c++) {
|
||||
if (strcmp(cache->qn[c], type_qn) == 0) {
|
||||
*out = cache->mro[c];
|
||||
return cache->len[c];
|
||||
}
|
||||
}
|
||||
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH)
|
||||
return -1;
|
||||
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn);
|
||||
if (rt && rt->alias_of)
|
||||
return py_c3_linearize(ctx, cache, rt->alias_of, out, depth + 1);
|
||||
const char **bases = rt ? rt->embedded_types : NULL;
|
||||
int nb = 0;
|
||||
while (bases && bases[nb])
|
||||
nb++;
|
||||
if (nb >= PY_C3_MAX)
|
||||
return -1;
|
||||
const char **seqs[PY_C3_MAX];
|
||||
int lens[PY_C3_MAX];
|
||||
int heads[PY_C3_MAX];
|
||||
for (int b = 0; b < nb; b++) {
|
||||
lens[b] = py_c3_linearize(ctx, cache, bases[b], &seqs[b], depth + 1);
|
||||
if (lens[b] < 0)
|
||||
return -1;
|
||||
heads[b] = 0;
|
||||
}
|
||||
seqs[nb] = bases;
|
||||
lens[nb] = nb;
|
||||
heads[nb] = 0;
|
||||
const char **mro = (const char **)cbm_arena_alloc(ctx->arena, PY_C3_MAX * sizeof(char *));
|
||||
if (!mro)
|
||||
return -1;
|
||||
int n = 0;
|
||||
mro[n++] = type_qn;
|
||||
for (;;) {
|
||||
bool pending = false;
|
||||
for (int s = 0; s <= nb && !pending; s++) {
|
||||
pending = heads[s] < lens[s];
|
||||
}
|
||||
if (!pending)
|
||||
break;
|
||||
const char *pick = py_c3_pick((const char **const *)seqs, lens, heads, nb + 1);
|
||||
if (!pick || n >= PY_C3_MAX)
|
||||
return -1;
|
||||
mro[n++] = pick;
|
||||
for (int s = 0; s <= nb; s++) {
|
||||
if (heads[s] < lens[s] && strcmp(seqs[s][heads[s]], pick) == 0)
|
||||
heads[s]++;
|
||||
}
|
||||
}
|
||||
if (cache->count < PY_C3_MAX) {
|
||||
cache->qn[cache->count] = type_qn;
|
||||
cache->mro[cache->count] = mro;
|
||||
cache->len[cache->count] = n;
|
||||
cache->count++;
|
||||
}
|
||||
*out = mro;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The method super() reaches from inside `class_qn`: the first class AFTER it in
|
||||
* its C3 order that declares `member` itself. *have_mro is false when the
|
||||
* hierarchy has no C3 order; the caller then keeps its base-by-base walk. */
|
||||
static const CBMRegisteredFunc *py_lookup_super_attribute(PyLSPContext *ctx, const char *class_qn,
|
||||
const char *member, bool *have_mro) {
|
||||
*have_mro = false;
|
||||
if (!ctx || !class_qn || !member)
|
||||
return NULL;
|
||||
py_c3_cache_t cache;
|
||||
cache.count = 0;
|
||||
const char **mro = NULL;
|
||||
int n = py_c3_linearize(ctx, &cache, class_qn, &mro, 0);
|
||||
if (n < 0)
|
||||
return NULL;
|
||||
*have_mro = true;
|
||||
for (int i = 1; i < n; i++) {
|
||||
const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, mro[i], member);
|
||||
if (f)
|
||||
return f;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Per-file field overlay: look up a (class_qn, field_name) recorded during resolve
|
||||
* against the sealed shared registry. Linear scan — the overlay only holds fields
|
||||
* discovered in THIS file's own classes, so it is small. */
|
||||
@@ -2964,6 +3096,22 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
|
||||
const CBMRegisteredType *enclosing =
|
||||
cbm_registry_lookup_type(ctx->registry, ctx->enclosing_class_qn);
|
||||
if (enclosing && enclosing->embedded_types) {
|
||||
// The proxy walks the enclosing class's C3 order, in which a
|
||||
// diamond's shared base follows every class deriving from it:
|
||||
// RadarrCalendarEntity(RadarrEntity, CalendarEntity) reaches
|
||||
// CalendarEntity before Entity, which the first base reaches
|
||||
// through its own ancestry. The base-by-base walk below stays for
|
||||
// a hierarchy without a C3 order and for an undeclared __init__.
|
||||
bool have_mro = false;
|
||||
const CBMRegisteredFunc *sf = py_lookup_super_attribute(
|
||||
ctx, ctx->enclosing_class_qn, attr_name, &have_mro);
|
||||
if (sf) {
|
||||
bool is_init = strcmp(attr_name, "__init__") == 0;
|
||||
py_emit_resolved_call(ctx, sf->qualified_name,
|
||||
is_init ? "lsp_super_init" : "lsp_super",
|
||||
is_init ? 0.90f : 0.88f, call_node);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; enclosing->embedded_types[i]; i++) {
|
||||
// super().__init__() is a constructor delegation:
|
||||
// lsp_super_init is the MORE SPECIFIC, more accurate
|
||||
|
||||
@@ -15211,6 +15211,114 @@ TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548) {
|
||||
return pyn_run_shadowing(52);
|
||||
}
|
||||
|
||||
/* ── super() follows the C3 method resolution order ──────────────── */
|
||||
|
||||
/* The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity,
|
||||
* CalendarEntity), where RadarrEntity reaches the common base Entity through
|
||||
* CoordinatorEntity and CalendarEntity overrides Entity's method. Python's C3
|
||||
* order is [RadarrCalendarEntity, RadarrEntity, CoordinatorEntity,
|
||||
* CalendarEntity, Entity]: a common base comes after every class that
|
||||
* derives from it, so super() reaches CalendarEntity, not Entity through the
|
||||
* first base's ancestry. */
|
||||
static const char pym_base_py[] = "class Entity:\n"
|
||||
" def __init__(self):\n"
|
||||
" self.ready = False\n"
|
||||
"\n"
|
||||
" def write_state(self):\n"
|
||||
" return 0\n"
|
||||
"\n"
|
||||
"class CoordinatorEntity(Entity):\n"
|
||||
" pass\n"
|
||||
"\n"
|
||||
"class CalendarEntity(Entity):\n"
|
||||
" def __init__(self):\n"
|
||||
" self.events = []\n"
|
||||
"\n"
|
||||
" def write_state(self):\n"
|
||||
" return 1\n";
|
||||
|
||||
static const char pym_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n"
|
||||
"\n"
|
||||
"class RadarrEntity(CoordinatorEntity):\n"
|
||||
" pass\n"
|
||||
"\n"
|
||||
"class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n"
|
||||
" def __init__(self):\n"
|
||||
" super().__init__()\n"
|
||||
"\n"
|
||||
" def write_state(self):\n"
|
||||
" return super().write_state()\n"
|
||||
"\n"
|
||||
"class PlainCalendar(CalendarEntity):\n"
|
||||
" def write_state(self):\n"
|
||||
" return super().write_state()\n";
|
||||
|
||||
static int pym_run_super_c3(int fillers) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_superc3_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "base.py", pym_base_py);
|
||||
write_temp_file(tmp, "radarr.py", pym_radarr_py);
|
||||
for (int i = 0; i < fillers; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "filler%d.py", i);
|
||||
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
if (fillers > 0) {
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
}
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/superc3.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int rc = p ? cbm_pipeline_run(p) : -1;
|
||||
cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL;
|
||||
const char *pr = p ? cbm_pipeline_project_name(p) : NULL;
|
||||
const char *rce = "radarr.RadarrCalendarEntity";
|
||||
char src[256];
|
||||
snprintf(src, sizeof(src), "%s.write_state", rce);
|
||||
bool m_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.write_state", "CALLS");
|
||||
bool m_dfs = s && pyn_edge(s, pr, src, "base.Entity.write_state", "CALLS");
|
||||
snprintf(src, sizeof(src), "%s.__init__", rce);
|
||||
bool i_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.__init__", "CALLS");
|
||||
bool i_dfs = s && pyn_edge(s, pr, src, "base.Entity.__init__", "CALLS");
|
||||
bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.write_state",
|
||||
"base.CalendarEntity.write_state", "CALLS");
|
||||
if (s) {
|
||||
cbm_store_close(s);
|
||||
}
|
||||
if (p) {
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_TRUE(m_c3); /* super().write_state(): CalendarEntity comes before Entity */
|
||||
ASSERT_FALSE(m_dfs); /* ... never Entity through RadarrEntity's ancestry */
|
||||
ASSERT_TRUE(i_c3); /* super().__init__(): the same order */
|
||||
ASSERT_FALSE(i_dfs);
|
||||
ASSERT_TRUE(plain); /* control: single inheritance */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_python_super_follows_c3_mro) {
|
||||
return pym_run_super_c3(0);
|
||||
}
|
||||
|
||||
TEST(pipeline_python_super_follows_c3_mro_parallel) {
|
||||
return pym_run_super_c3(52);
|
||||
}
|
||||
|
||||
/* L7 review of #1548: a package __init__.py function can share a submodule's QN
|
||||
* (poetry: layouts/__init__.py `def layout` is the QN prefix of every symbol in
|
||||
* layouts/layout.py). The function-local detection is bounded at the symbol's
|
||||
@@ -24914,6 +25022,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548);
|
||||
RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548);
|
||||
RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548);
|
||||
RUN_TEST(pipeline_python_super_follows_c3_mro);
|
||||
RUN_TEST(pipeline_python_super_follows_c3_mro_parallel);
|
||||
RUN_TEST(pipeline_python_init_function_shares_submodule_qn_issue1548);
|
||||
RUN_TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548);
|
||||
RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548);
|
||||
|
||||
Reference in New Issue
Block a user