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:
Martin Vogel
2026-10-19 02:55:13 +02:00
committed by GitHub
2 changed files with 258 additions and 0 deletions
+148
View File
@@ -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
+110
View File
@@ -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);