Merge pull request #2695 from DeusData/fix/py-class-body-sibling-base

fix(python): bind a nested class base to an earlier sibling of its class body
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-19 03:24:32 +02:00
commit 90d6d64c62
6 files changed
+247 -20

No files matched your search

+15 -7
View File
@@ -305,10 +305,16 @@ static const char *pxc_python_dotted_base_qn(const pxc_base_scope_t *bs, const c
* Returns NULL when the base is not a confidently-known project type;
* stdlib and third-party bases land here and keep their raw spelling. */
static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *raw,
const char *module_qn) {
const char *module_qn, const char *class_qn) {
if (!bs || !bs->reg || !raw || !raw[0]) {
return NULL;
}
/* A base of a class nested in a class body: an earlier sibling first. */
const char *sibling =
cbm_registry_resolve_python_class_body_base(bs->reg, bs->gbuf, class_qn, raw, bs->lang);
if (sibling) {
return sibling;
}
const char *dotted = pxc_python_dotted_base_qn(bs, raw);
if (dotted) {
return dotted;
@@ -338,7 +344,8 @@ static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *r
* source spelling. Unresolved entries pass through verbatim so a base the
* registry does not know keeps working exactly as before. */
static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
const pxc_base_scope_t *bs, const char *module_qn) {
const pxc_base_scope_t *bs, const char *module_qn,
const char *class_qn) {
if (!bases || !bases[0]) {
return NULL;
}
@@ -352,7 +359,7 @@ static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
return pxc_join_pipe(arena, bases);
}
for (int i = 0; i < count; i++) {
const char *qn = pxc_resolve_base_qn(bs, bases[i], module_qn);
const char *qn = pxc_resolve_base_qn(bs, bases[i], module_qn, class_qn);
resolved[i] = qn ? qn : bases[i];
}
resolved[count] = NULL;
@@ -520,9 +527,10 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
/* Languages whose cross registrars read embedded_types as QNs get their
* bases resolved against the project registry; everyone else keeps the
* raw source spelling their own registrar already knows how to handle. */
dst->embedded_types = (bs && bs->reg && pxc_lang_resolves_base_qns(lang))
? pxc_join_base_qns(arena, src->base_classes, bs, module_qn)
: pxc_join_pipe(arena, src->base_classes);
dst->embedded_types =
(bs && bs->reg && pxc_lang_resolves_base_qns(lang))
? pxc_join_base_qns(arena, src->base_classes, bs, module_qn, src->qualified_name)
: pxc_join_pipe(arena, src->base_classes);
dst->signature_param_types = NULL;
dst->signature_param_count = 0;
if (src->signature_param_types && src->signature_param_count > 0) {
@@ -727,7 +735,7 @@ static void pxc_fold_py_field_types(CBMArena *arena, const CBMFileResult *result
fields[f].type_qn = NULL;
CBMLSPDef *owner = ft->class_qn ? (CBMLSPDef *)cbm_ht_get(by_qn, ft->class_qn) : NULL;
const char *name = owner ? pxc_py_annotation_type_name(arena, ft->type_text) : NULL;
const char *qn = name ? pxc_resolve_base_qn(bs, name, module_qn) : NULL;
const char *qn = name ? pxc_resolve_base_qn(bs, name, module_qn, NULL) : NULL;
if (!qn || !ft->field_name || !ft->field_name[0]) {
continue;
}
+11 -7
View File
@@ -3910,13 +3910,17 @@ static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (cbm_pipeline_php_vendor_bound(php_vendor, def->base_classes[b])) {
continue;
}
const char *bqn =
resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic, fs->lang);
/* Same external-base gate as the sequential semantic pass. */
if (!bqn ||
cbm_python_external_base_contradicts(fs->lang, fs->imports, def->base_classes[b], bqn,
rc->main_gbuf, rc->project_name, fs->rel)) {
continue;
/* Same order as the sequential semantic pass: an earlier sibling of
* the class body first (no external-base gate), then the registry. */
const char *bqn = cbm_registry_resolve_python_class_body_base(
rc->registry, rc->main_gbuf, def->qualified_name, def->base_classes[b], fs->lang);
if (!bqn) {
bqn = resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic, fs->lang);
if (!bqn || cbm_python_external_base_contradicts(
fs->lang, fs->imports, def->base_classes[b], bqn, rc->main_gbuf,
rc->project_name, fs->rel)) {
continue;
}
}
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, bqn);
if (bn && node->id != bn->id) {
+12 -6
View File
@@ -643,12 +643,18 @@ static void sem_process_def_edges(cbm_pipeline_ctx_t *ctx, const CBMDefinition *
if (cbm_pipeline_php_vendor_bound(php_vendor, def->base_classes[b])) {
continue;
}
const char *base_qn = resolve_as_class(ctx->registry, def->base_classes[b], module_qn,
imp_keys, imp_vals, imp_count, lang);
if (!base_qn || cbm_python_external_base_contradicts(
fi->language, &result->imports, def->base_classes[b], base_qn,
ctx->gbuf, ctx->project_name, fi->rel_path)) {
continue; /* unresolved, or an external base (`unittest.TestCase`) */
/* An earlier sibling of the class body shadows every module or
* imported name, so it needs no external-base gate. */
const char *base_qn = cbm_registry_resolve_python_class_body_base(
ctx->registry, ctx->gbuf, def->qualified_name, def->base_classes[b], lang);
if (!base_qn) {
base_qn = resolve_as_class(ctx->registry, def->base_classes[b], module_qn, imp_keys,
imp_vals, imp_count, lang);
if (!base_qn || cbm_python_external_base_contradicts(
fi->language, &result->imports, def->base_classes[b], base_qn,
ctx->gbuf, ctx->project_name, fi->rel_path)) {
continue; /* unresolved, or an external base (`unittest.TestCase`) */
}
}
const cbm_gbuf_node_t *base_node = cbm_gbuf_find_by_qn(ctx->gbuf, base_qn);
if (base_node && node->id != base_node->id) {
+8
View File
@@ -270,6 +270,14 @@ cbm_resolution_t cbm_registry_resolve_call_scoped(const cbm_registry_t *r, const
const char **import_map_vals,
int import_map_count, CBMLanguage lang,
const char *scope_qn);
/* Python: the bare base `base` of the class `class_qn` nested in a class body
* binds the sibling class of that body (`<enclosing class>.<base>`) when the
* sibling is defined earlier in the same file (Python evaluates the bases in
* the class body). Exact lookups only; the registry-owned sibling QN, or NULL
* to resolve the base as before. Every base-class resolver asks this first. */
const char *cbm_registry_resolve_python_class_body_base(const cbm_registry_t *r,
const cbm_gbuf_t *gb, const char *class_qn,
const char *base, CBMLanguage lang);
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_pxc_test_import_probe_fail_after(int successful_allocations);
#endif
+41
View File
@@ -1383,6 +1383,47 @@ const char *cbm_registry_value_target(const cbm_registry_t *r, const char *qn) {
return owned ? owned : qn;
}
/* Python evaluates a class statement's bases in the namespace of the body
* that encloses it. For a class nested in a class, a bare base naming a class
* defined EARLIER in that same class body is that sibling
* (`class IntegrityError(DatabaseError)` in sqlalchemy's asyncpg adapter),
* whatever same-named classes the project has elsewhere. A sibling defined
* later is not bound yet when the base is evaluated. Exact lookups only. */
const char *cbm_registry_resolve_python_class_body_base(const cbm_registry_t *r,
const cbm_gbuf_t *gb, const char *class_qn,
const char *base, CBMLanguage lang) {
if (lang != CBM_LANG_PYTHON || !r || !gb || !class_qn || !base || !base[0] ||
strchr(base, '.')) {
return NULL;
}
const char *dot = strrchr(class_qn, '.');
if (!dot || dot == class_qn) {
return NULL;
}
char sibling[CBM_SZ_1K];
int enclosing_len = (int)(dot - class_qn);
int n = snprintf(sibling, sizeof(sibling), "%.*s", enclosing_len, class_qn);
if (n < 0 || (size_t)n >= sizeof(sibling) ||
!cbm_label_is_type_like(cbm_registry_label_of(r, sibling))) {
return NULL; /* not nested in a class body (module level, or a def) */
}
n = snprintf(sibling, sizeof(sibling), "%.*s.%s", enclosing_len, class_qn, base);
if (n < 0 || (size_t)n >= sizeof(sibling) || strcmp(sibling, class_qn) == 0) {
return NULL;
}
const char *owned = cbm_ht_get_key(r->exact, sibling);
if (!owned || !cbm_label_is_type_like(cbm_registry_label_of(r, owned))) {
return NULL;
}
const cbm_gbuf_node_t *cls = cbm_gbuf_find_by_qn(gb, class_qn);
const cbm_gbuf_node_t *sib = cbm_gbuf_find_by_qn(gb, owned);
if (!cls || !sib || !cls->file_path || !sib->file_path ||
strcmp(cls->file_path, sib->file_path) != 0 || sib->start_line >= cls->start_line) {
return NULL;
}
return owned;
}
int cbm_registry_find_by_name(const cbm_registry_t *r, const char *name, const char ***out,
int *count) {
if (!r || !out || !count) {
+160
View File
@@ -15416,6 +15416,164 @@ TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548) {
return pyn_run_init_collision(52);
}
/* ── F15: a nested class's bare base resolves in its class body first ── */
/* The sqlalchemy asyncpg shape: an adapter class re-declares the DBAPI
* exception hierarchy as nested classes, each based on an EARLIER sibling
* spelled bare (`class IntegrityError(DatabaseError)`). Python evaluates a
* class statement's bases in the enclosing class body, so the sibling wins
* over the same-named module classes of exc.py, which the adapter imports
* as a module (so a name guess prefers them, as on sqlalchemy once its
* relative `from ... import exc` resolves). A sibling defined LATER in the
* body is not bound yet: `Early(Later)` names the module-level Later. */
static const char pyb_exc_py[] = "class DBAPIError(Exception):\n"
" pass\n"
"\n"
"class DatabaseError(DBAPIError):\n"
" pass\n"
"\n"
"class IntegrityError(DatabaseError):\n"
" pass\n";
static const char pyb_adapter_py[] = "import exc\n"
"from exc import DBAPIError\n"
"\n"
"class Later(Exception):\n"
" pass\n"
"\n"
"class AsyncAdapt_dbapi:\n"
" class Error(DBAPIError):\n"
" pass\n"
"\n"
" class DatabaseError(Error):\n"
" pass\n"
"\n"
" class IntegrityError(DatabaseError):\n"
" pass\n"
"\n"
" class NotNullViolationError(IntegrityError):\n"
" pass\n"
"\n"
" class Early(Later):\n"
" pass\n"
"\n"
" class Later(Exception):\n"
" pass\n";
/* A second adapter with the same nested names, as sqlalchemy has one per
* driver: a name-only guess has several same-named candidates to pick. */
static const char pyb_adapter2_py[] = "class AsyncAdapt_other_dbapi:\n"
" class Error(Exception):\n"
" pass\n"
"\n"
" class DatabaseError(Error):\n"
" pass\n"
"\n"
" class IntegrityError(DatabaseError):\n"
" pass\n";
static int pyb_run_class_body_base(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_cbbase_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "exc.py", pyb_exc_py);
write_temp_file(tmp, "adapter.py", pyb_adapter_py);
write_temp_file(tmp, "adapter2.py", pyb_adapter2_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/cbbase.db", tmp);
int rc[2] = {-1, -1};
bool db_sib[2] = {false, false};
bool db_mod[2] = {true, true};
bool ie_sib[2] = {false, false};
bool ie_mod[2] = {true, true};
bool nn_sib[2] = {false, false};
bool err_imp[2] = {false, false};
bool early_mod[2] = {false, false};
bool early_later_sib[2] = {true, true};
bool other_sib[2] = {false, false};
for (int run = 0; run < 2; run++) {
if (run == 1) {
char path[512];
snprintf(path, sizeof(path), "%s/adapter.py", tmp);
th_append_file(path, "\n# edited: the second run re-indexes incrementally\n");
}
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
rc[run] = p ? cbm_pipeline_run(p) : -1;
cbm_store_t *s = rc[run] == 0 ? cbm_store_open_path(db_path) : NULL;
if (s) {
const char *pr = cbm_pipeline_project_name(p);
const char *cls = "adapter.AsyncAdapt_dbapi";
char a[256];
char b[256];
snprintf(a, sizeof(a), "%s.DatabaseError", cls);
snprintf(b, sizeof(b), "%s.Error", cls);
db_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
db_mod[run] = pyn_edge(s, pr, a, "adapter2.AsyncAdapt_other_dbapi.Error", "INHERITS");
snprintf(a, sizeof(a), "%s.IntegrityError", cls);
snprintf(b, sizeof(b), "%s.DatabaseError", cls);
ie_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
ie_mod[run] = pyn_edge(s, pr, a, "exc.DatabaseError", "INHERITS");
snprintf(a, sizeof(a), "%s.NotNullViolationError", cls);
snprintf(b, sizeof(b), "%s.IntegrityError", cls);
nn_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
snprintf(a, sizeof(a), "%s.Error", cls);
err_imp[run] = pyn_edge(s, pr, a, "exc.DBAPIError", "INHERITS");
snprintf(a, sizeof(a), "%s.Early", cls);
snprintf(b, sizeof(b), "%s.Later", cls);
early_mod[run] = pyn_edge(s, pr, a, "adapter.Later", "INHERITS");
early_later_sib[run] = pyn_edge(s, pr, a, b, "INHERITS");
other_sib[run] = pyn_edge(s, pr, "adapter2.AsyncAdapt_other_dbapi.IntegrityError",
"adapter2.AsyncAdapt_other_dbapi.DatabaseError", "INHERITS");
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);
for (int run = 0; run < 2; run++) {
ASSERT_EQ(rc[run], 0);
ASSERT_TRUE(db_sib[run]); /* DatabaseError(Error): the earlier sibling */
ASSERT_FALSE(db_mod[run]); /* ... not the other adapter's Error */
ASSERT_TRUE(ie_sib[run]); /* IntegrityError(DatabaseError): the sibling */
ASSERT_FALSE(ie_mod[run]); /* ... not exc.DatabaseError */
ASSERT_TRUE(nn_sib[run]); /* a chain of siblings */
ASSERT_TRUE(err_imp[run]); /* control: a non-sibling base keeps the import */
ASSERT_TRUE(early_mod[run]); /* a LATER sibling is not bound yet: module Later */
ASSERT_FALSE(early_later_sib[run]); /* ... never the later sibling */
ASSERT_TRUE(other_sib[run]); /* the second adapter binds its own sibling */
}
PASS();
}
TEST(pipeline_python_class_body_sibling_base) {
return pyb_run_class_body_base(0);
}
TEST(pipeline_python_class_body_sibling_base_parallel) {
return pyb_run_class_body_base(52);
}
/* #1548 keeps TESTS for calls made through a test's own nested helper: the
* call now starts at the extracted helper def (not at the test FILE node, as
* before #1548), so TESTS is attributed to the enclosing test function. A
@@ -25114,6 +25272,8 @@ SUITE(pipeline) {
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_class_body_sibling_base);
RUN_TEST(pipeline_python_class_body_sibling_base_parallel);
RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548);
RUN_TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548);
RUN_TEST(pipeline_python_decorated_nested_class_extracted);