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
80 changes: 48 additions & 32 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -223,25 +223,47 @@ static bool pxc_base_strategy_is_weak(const char *strategy) {
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}

/* One file's base-resolution scope: the registry + import map pass_semantic
* uses, plus the facts the external-base gate reads. NULL (the surface-probe
* path) keeps every base at its raw spelling. */
typedef struct {
const cbm_registry_t *reg;
const char **imp_keys;
const char **imp_vals;
int imp_count;
const CBMImportArray *imports;
const cbm_gbuf_t *gbuf;
const char *project_name;
const char *rel;
CBMLanguage lang;
} pxc_base_scope_t;

/* Resolve one base-class spelling to a project QN. Mirrors
* pass_semantic.c::resolve_as_class — same registry, same type-like veto —
* then additionally rejects weak short-name strategies (see above).
* 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 cbm_registry_t *reg, const char *raw,
const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
if (!reg || !raw || !raw[0]) {
static const char *pxc_resolve_base_qn(const pxc_base_scope_t *bs, const char *raw,
const char *module_qn) {
if (!bs || !bs->reg || !raw || !raw[0]) {
return NULL;
}
cbm_resolution_t res = cbm_registry_resolve(reg, raw, module_qn, imp_keys, imp_vals, imp_count);
cbm_resolution_t res =
cbm_registry_resolve(bs->reg, raw, module_qn, bs->imp_keys, bs->imp_vals, bs->imp_count);
if (!res.qualified_name || !res.qualified_name[0]) {
return NULL;
}
if (pxc_base_strategy_is_weak(res.strategy)) {
return NULL;
}
if (!cbm_label_is_type_like(cbm_registry_label_of(reg, res.qualified_name))) {
if (!cbm_label_is_type_like(cbm_registry_label_of(bs->reg, res.qualified_name))) {
return NULL;
}
/* `unittest.TestCase` under an external `import unittest` survives the
* weak-strategy veto through the same-module suffix fallback; the shared
* external-base gate keeps it at its source spelling. */
if (cbm_python_external_base_contradicts(bs->lang, bs->imports, raw, res.qualified_name,
bs->gbuf, bs->project_name, bs->rel)) {
return NULL;
}
return res.qualified_name;
Expand All @@ -251,8 +273,7 @@ static const char *pxc_resolve_base_qn(const cbm_registry_t *reg, const char *ra
* 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 cbm_registry_t *reg, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count) {
const pxc_base_scope_t *bs, const char *module_qn) {
if (!bases || !bases[0]) {
return NULL;
}
Expand All @@ -266,8 +287,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(reg, bases[i], module_qn, imp_keys, imp_vals, imp_count);
const char *qn = pxc_resolve_base_qn(bs, bases[i], module_qn);
resolved[i] = qn ? qn : bases[i];
}
resolved[count] = NULL;
Expand Down Expand Up @@ -403,8 +423,7 @@ static const char *pxc_qn_leaf(const char *name) {
* pointers into src and into `arena` for synthesised composites. */
static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const char *module_qn,
const char *namespace_name, CBMLanguage lang, CBMLSPDef *dst,
const cbm_registry_t *reg, const char **imp_keys,
const char **imp_vals, int imp_count) {
const pxc_base_scope_t *bs) {
const char *label = pxc_map_label(arena, src->label);
if (!label || !src->qualified_name || !src->name)
return -1;
Expand All @@ -430,9 +449,8 @@ 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 = (reg && pxc_lang_resolves_base_qns(lang))
? pxc_join_base_qns(arena, src->base_classes, reg, module_qn,
imp_keys, imp_vals, imp_count)
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->signature_param_types = NULL;
dst->signature_param_count = 0;
Expand Down Expand Up @@ -610,11 +628,10 @@ typedef struct {
} pxc_py_field_t;

static void pxc_fold_py_field_types(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs,
int start, int end, const cbm_registry_t *reg,
const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
int start, int end, const pxc_base_scope_t *bs,
const char *module_qn) {
int nf = result ? result->field_types.count : 0;
if (!arena || nf == 0 || !defs || start >= end || !reg) {
if (!arena || nf == 0 || !defs || start >= end || !bs || !bs->reg) {
return;
}
CBMHashTable *by_qn = cbm_ht_create((uint32_t)(end - start));
Expand All @@ -639,8 +656,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(reg, name, module_qn, imp_keys, imp_vals, imp_count) : NULL;
const char *qn = name ? pxc_resolve_base_qn(bs, name, module_qn) : NULL;
if (!qn || !ft->field_name || !ft->field_name[0]) {
continue;
}
Expand Down Expand Up @@ -782,27 +798,27 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMArena *are
* that consume resolved base QNs, and only when a caller supplied the
* pipeline context (the surface-probe path passes NULL and keeps the
* raw spelling). */
const cbm_registry_t *base_reg = NULL;
const char **imp_keys = NULL;
const char **imp_vals = NULL;
int imp_count = 0;
pxc_base_scope_t bs = {.imports = &fr->imports,
.project_name = project_name,
.rel = files[fi].rel_path,
.lang = files[fi].language};
if (ctx && ctx->registry && pxc_lang_resolves_base_qns(files[fi].language)) {
base_reg = ctx->registry;
bs.reg = ctx->registry;
bs.gbuf = ctx->gbuf;
cbm_pxc_build_import_map(ctx->gbuf, project_name, files[fi].rel_path,
files[fi].language, fr, &imp_keys, &imp_vals, &imp_count);
files[fi].language, fr, &bs.imp_keys, &bs.imp_vals,
&bs.imp_count);
}
for (int di = 0; di < fr->defs.count; di++) {
if (pxc_build_lsp_def(arena, &fr->defs.items[di], def_modules[fi], namespace_name,
files[fi].language, &defs[idx], base_reg, imp_keys, imp_vals,
imp_count) == 0) {
files[fi].language, &defs[idx], bs.reg ? &bs : NULL) == 0) {
idx++;
}
}
if (files[fi].language == CBM_LANG_PYTHON) {
pxc_fold_py_field_types(arena, fr, defs, file_start, idx, base_reg, def_modules[fi],
imp_keys, imp_vals, imp_count);
pxc_fold_py_field_types(arena, fr, defs, file_start, idx, &bs, def_modules[fi]);
}
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
cbm_pxc_free_import_map(bs.imp_keys, bs.imp_vals, bs.imp_count); /* NULL-safe */
if (files[fi].language == CBM_LANG_GO) {
pxc_fold_struct_fields(arena, fr, defs, file_start, idx, "Struct");
} else if (files[fi].language == CBM_LANG_C || files[fi].language == CBM_LANG_CPP) {
Expand Down
25 changes: 19 additions & 6 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3659,16 +3659,27 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
cbm_pipeline_lsp_field_index_free(&field_index);
}

/* The per-file facts the base-class gate needs beyond the import map. */
typedef struct {
const CBMImportArray *imports;
const char *rel;
CBMLanguage lang;
} pp_file_scope_t;

/* Resolve base_classes → INHERITS edges for one definition. */
static void resolve_def_inherits(resolve_ctx_t *rc, resolve_worker_state_t *ws,
const CBMDefinition *def, const cbm_gbuf_node_t *node,
const char *mq, const char **ik, const char **iv, int ic) {
const pp_file_scope_t *fs, const char *mq, const char **ik,
const char **iv, int ic) {
if (!def->base_classes) {
return;
}
for (int b = 0; def->base_classes[b]; b++) {
const char *bqn = resolve_as_class(rc->registry, def->base_classes[b], mq, ik, iv, ic);
if (!bqn) {
/* 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;
}
const cbm_gbuf_node_t *bn = cbm_gbuf_find_by_qn(rc->main_gbuf, bqn);
Expand Down Expand Up @@ -3755,8 +3766,10 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws

/* Resolve INHERITS + DECORATES + IMPLEMENTS for one file. */
static void resolve_file_semantic(resolve_ctx_t *rc, resolve_worker_state_t *ws,
CBMFileResult *result, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count) {
CBMFileResult *result, const char *rel, CBMLanguage lang,
const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
const pp_file_scope_t fs = {&result->imports, rel, lang};
for (int d = 0; d < result->defs.count; d++) {
CBMDefinition *def = &result->defs.items[d];
if (!def->qualified_name) {
Expand All @@ -3766,7 +3779,7 @@ static void resolve_file_semantic(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (!node) {
continue;
}
resolve_def_inherits(rc, ws, def, node, module_qn, imp_keys, imp_vals, imp_count);
resolve_def_inherits(rc, ws, def, node, &fs, module_qn, imp_keys, imp_vals, imp_count);
resolve_def_decorators(rc, ws, def, node, module_qn, imp_keys, imp_vals, imp_count);
}
for (int t = 0; t < result->impl_traits.count; t++) {
Expand Down Expand Up @@ -4188,7 +4201,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {

/* ── INHERITS + DECORATES + IMPLEMENTS ──────────────────── */
_ph_t0 = extract_now_ns();
resolve_file_semantic(rc, ws, result, module_qn, imp_keys, imp_vals, imp_count);
resolve_file_semantic(rc, ws, result, rel, lang, module_qn, imp_keys, imp_vals, imp_count);
atomic_fetch_add_explicit(&rc->time_ns_semantic, extract_now_ns() - _ph_t0,
memory_order_relaxed);

Expand Down
26 changes: 26 additions & 0 deletions src/pipeline/pass_pkgmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,11 @@ bool cbm_python_import_binding_contradicts(const CBMImportArray *imports, const
/* `import a.b` binds the ROOT package `a`: the callee already
* spells its own full dotted path (`a.b.f()`). */
snprintf(full, sizeof(full), "%s", callee_name);
} else if (!as && !path_leaf_seg && full[0] != '.' && strcmp(full, imp->local_name) != 0) {
/* `import m as y` (the extractor stores module_path `m`, local
* `y`; a from-import always spells a dotted path): `y` IS module
* `m`, so `y.T` spells `m.T`. */
snprintf(full, sizeof(full), "%s%s", imp->module_path, callee_name + root_len);
}
/* Otherwise (`from m import x [as y]`) only the import's own module
* chain is evidence: members reached THROUGH x (`x.objects.create`)
Expand All @@ -1715,6 +1720,27 @@ bool cbm_python_import_binding_contradicts(const CBMImportArray *imports, const
return bound;
}

/* Base-class twin of the #2127 call gate. `class SimpleTestCase(
* unittest.TestCase)` under `import unittest` names the STDLIB base; the
* registry's same-module suffix fallback (`unittest.TestCase` -> module's
* `TestCase`) or a short-name strategy bound it to a same-named PROJECT class
* (django: an inheritance cycle SimpleTestCase -> TestCase ->
* TransactionTestCase -> SimpleTestCase, plus fabricated OVERRIDEs). An
* external import binding (never materialized as an IMPORTS edge) whose
* module chain the target does not spell can never be that target, whatever
* the registry strategy. Exact metadata import maps remain valid when the
* target spells the imported module chain. Every base resolver (sequential
* + parallel semantic, cross-LSP pxc) calls this ONE
* gate so the venues cannot diverge. Python only. */
bool cbm_python_external_base_contradicts(CBMLanguage lang, const CBMImportArray *imports,
const char *base_spelling, const char *base_qn,
const cbm_gbuf_t *gbuf, const char *project_name,
const char *rel_path) {
return lang == CBM_LANG_PYTHON &&
cbm_python_import_binding_contradicts(imports, base_spelling, base_qn, gbuf,
project_name, rel_path);
}

/* #2127: whether a Python import's module lives in this project. Relative
* imports always do; otherwise the module part (the path minus the imported
* name) must resolve to a graph node. Strategy 1 has already failed on the
Expand Down
Loading
Loading