mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
6 files changed
+247
-20
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user