fix(python): bind a nested class base to an earlier sibling of its class body

Python evaluates a class statement's bases in the body that encloses it, so
a bare base of a class nested in a class names an earlier sibling of that
body (sqlalchemy's asyncpg adapter: class IntegrityError(DatabaseError))
before any same-named module or imported class. The base resolvers ran the
registry's name strategies instead, which prefer an import-reachable
candidate, so the sibling lost as soon as the adapter's exc import resolved.

cbm_registry_resolve_python_class_body_base looks the sibling up exactly
(<enclosing class>.<base>, both type-like) and requires it to be defined
earlier in the same file, from the graph-buffer node lines: a later sibling
is not bound yet when the base is evaluated. The sequential and incremental
semantic pass, the parallel resolver and the cross-LSP base resolver ask it
first; a sibling needs no external-base gate.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-18 22:17:23 +02:00
1 parent 9054309fee
commit 79cb4f7227
5 files changed
+87 -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
@@ -3863,13 +3863,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) {