Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
18 changes: 11 additions & 7 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 12 additions & 6 deletions src/pipeline/pass_semantic.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions src/pipeline/pipeline_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
160 changes: 160 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -15220,6 +15220,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
Expand Down Expand Up @@ -24826,6 +24984,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548);
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_header_include_target_is_independent_of_registration_order);
Expand Down
Loading