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
149 changes: 142 additions & 7 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,114 @@ static const char *pxc_join_pipe(CBMArena *arena, const char *const *items) {
return buf;
}

/* ── Cross-file base-class QN resolution ──────────────────────────
*
* CBMDefinition.base_classes carries the SOURCE SPELLING of each base
* ("Base", "django.db.Model", "React.Component"): extraction strips
* keywords and generic arguments, but it cannot know WHERE the name is
* declared. The Python and TS cross-file registrars, however, consume
* CBMLSPDef.embedded_types as fully-qualified names — py_lookup_attribute
* and ts_lookup_member feed each entry straight into
* cbm_registry_lookup_type. An unqualified spelling therefore matched
* nothing declared in ANOTHER file: `class Child(Base)` in child.py never
* saw Base in base.py, so a call to an inherited method through a typed
* receiver had no member to find and fell through to the weak textual
* cascade (where the #592/#606 guard correctly kills it).
*
* Resolve each base name ONCE per definition here, from exactly the
* inputs pass_semantic uses to draw its INHERITS edge: the project
* registry, the declaring module, and the file's import map. Two
* properties follow. The LSP's inheritance view is the same relation the
* graph records, so the two cannot diverge. And the binding is
* import- or same-module-backed, not a short-name guess, so the CALLS
* edge it enables is a supported fact that the weak-member guard keeps.
*
* Cost: O(defs) hash lookups + ONE import map per file. No per-call-site
* hierarchy walk, no registry scan, no per-file registry rebuild.
*/
static bool pxc_lang_resolves_base_qns(CBMLanguage lang) {
switch (lang) {
case CBM_LANG_PYTHON:
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
return true;
default:
/* Go / JVM / C# / C++ / Rust registrars qualify their own embedded
* types already (struct embedding, parent_class chains, impl-trait
* provenance). Re-resolving here would fight those paths, so they
* keep the raw join. */
return false;
}
}

/* True for the registry strategies that are pure short-name guesses.
* EXPLICIT drop-list, mirroring cbm_tsjs_suppress_weak_method_match: a
* base class bound by "some project type happens to share this name" is
* exactly the fabricated relation #606 removed, and inheritance
* multiplies it — every inherited member of the wrong base would become
* a callable target. Import-, module- and suffix-aware strategies are
* kept; anything unresolved simply retains its source spelling and the
* behaviour that predates this resolution. */
static bool pxc_base_strategy_is_weak(const char *strategy) {
if (!strategy || !strategy[0]) {
return true;
}
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}

/* 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]) {
return NULL;
}
cbm_resolution_t res = cbm_registry_resolve(reg, raw, module_qn, imp_keys, imp_vals, 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))) {
return NULL;
}
return res.qualified_name;
}

/* pxc_join_pipe over base_classes, substituting each resolved QN for its
* 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) {
if (!bases || !bases[0]) {
return NULL;
}
int count = 0;
while (bases[count]) {
count++;
}
const char **resolved =
(const char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const char *));
if (!resolved) {
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);
resolved[i] = qn ? qn : bases[i];
}
resolved[count] = NULL;
return pxc_join_pipe(arena, resolved);
}

static bool pxc_is_jvm_lang(CBMLanguage lang);

static const char *pxc_last_component(const char *qn) {
Expand Down Expand Up @@ -262,7 +370,9 @@ static const char *pxc_qn_leaf(const char *name) {
* to skip (unsupported label or missing required field). dst gets borrowed
* 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 char *namespace_name, CBMLanguage lang, CBMLSPDef *dst,
const cbm_registry_t *reg, const char **imp_keys,
const char **imp_vals, int imp_count) {
const char *label = pxc_map_label(src->label);
if (!label || !src->qualified_name || !src->name)
return -1;
Expand All @@ -283,7 +393,13 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
* for multi-return languages (Go); single-return languages just see one
* piece, which is what's already stored. */
dst->return_types = src->return_type;
dst->embedded_types = pxc_join_pipe(arena, src->base_classes);
/* 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)
: pxc_join_pipe(arena, src->base_classes);
dst->signature_param_types = src->signature_param_types;
dst->signature_param_count = src->signature_param_count;
dst->lang = lang;
Expand Down Expand Up @@ -323,9 +439,10 @@ static int pxc_build_rust_impl_relation(CBMArena *arena, const CBMImplTrait *imp
/* Collect a project-wide CBMLSPDef[] from all cached results. Returns a
* malloc'd array (caller frees) of length *out_count. String fields are
* borrowed from cache[i]->arena and from def_modules[i] (also borrowed). */
CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t *files,
int file_count, const char *project_name, char **def_modules,
int *out_count, int *out_def_starts) {
CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
const cbm_file_info_t *files, int file_count,
const char *project_name, char **def_modules, int *out_count,
int *out_def_starts) {
int total = 0;
for (int i = 0; i < file_count; i++) {
if (cache[i]) {
Expand Down Expand Up @@ -369,12 +486,30 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
cache[fi]->namespace_name = namespace_name;
}
}
/* One import map per FILE (not per def, and not per base name): the
* cross-file base-class resolution below needs the same local-name →
* import-QN view pass_semantic uses. Built only for the languages
* 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;
if (ctx && ctx->registry && pxc_lang_resolves_base_qns(files[fi].language)) {
base_reg = ctx->registry;
cbm_pxc_build_import_map(ctx->gbuf, project_name, files[fi].rel_path,
files[fi].language, cache[fi], &imp_keys, &imp_vals,
&imp_count);
}
for (int di = 0; di < cache[fi]->defs.count; di++) {
if (pxc_build_lsp_def(&cache[fi]->arena, &cache[fi]->defs.items[di], def_modules[fi],
namespace_name, files[fi].language, &defs[idx]) == 0) {
namespace_name, files[fi].language, &defs[idx], base_reg,
imp_keys, imp_vals, imp_count) == 0) {
idx++;
}
}
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
if (files[fi].language == CBM_LANG_RUST) {
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
if (pxc_build_rust_impl_relation(
Expand Down Expand Up @@ -1284,7 +1419,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *

int def_count = 0;
int *def_starts = (int *)calloc((size_t)file_count + 1, sizeof(int));
CBMLSPDef *all_defs = cbm_pxc_collect_all_defs(cache, files, file_count, ctx->project_name,
CBMLSPDef *all_defs = cbm_pxc_collect_all_defs(ctx, cache, files, file_count, ctx->project_name,
def_modules, &def_count, def_starts);
/* Same seam as the parallel driver: serialize per-file surfaces while the
* result cache is alive. Failure only degrades to a full rebuild on the
Expand Down
17 changes: 13 additions & 4 deletions src/pipeline/pass_lsp_cross.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,19 @@ bool cbm_pxc_has_cross_lsp(CBMLanguage lang);
* receives per-file prefix offsets: file i's defs occupy
* [out_def_starts[i], out_def_starts[i+1]) — the LSP-surface serializer
* needs the per-file slices, which the flat array does not otherwise
* record. */
CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t *files,
int file_count, const char *project_name, char **def_modules,
int *out_count, int *out_def_starts);
* record.
*
* `ctx` (nullable) enables cross-file base-class resolution: for the
* languages whose cross registrars read embedded_types as qualified names
* (Python, JS/TS/TSX), every CBMDefinition.base_classes spelling is resolved
* to a project QN through ctx->registry plus the file's import map — the same
* inputs pass_semantic uses to draw its INHERITS edge, so the LSP's
* inheritance view and the graph's cannot diverge. Pass NULL to keep the raw
* source spelling (surface-probe paths that build no registry). */
CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
const cbm_file_info_t *files, int file_count,
const char *project_name, char **def_modules, int *out_count,
int *out_def_starts);

/* Detect TS dialect flags from a relative path. */
void cbm_pxc_ts_modes(CBMLanguage lang, const char *rel_path, bool *out_js, bool *out_jsx,
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
def_modules = (char **)calloc((size_t)file_count, sizeof(char *));
def_starts = (int *)calloc((size_t)file_count + 1, sizeof(int));
all_defs = def_modules
? cbm_pxc_collect_all_defs(cache, files, file_count, ctx->project_name,
? cbm_pxc_collect_all_defs(ctx, cache, files, file_count, ctx->project_name,
def_modules, &def_count, def_starts)
: NULL;
}
Expand Down
6 changes: 3 additions & 3 deletions src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,7 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
int fresh_count = 0;
CBMLSPDef *fresh_defs =
def_modules && def_starts
? cbm_pxc_collect_all_defs(cache, changed_files, ci, ctx->project_name,
? cbm_pxc_collect_all_defs(ctx, cache, changed_files, ci, ctx->project_name,
def_modules, &fresh_count, def_starts)
: NULL;
if ((fresh_defs || fresh_count == 0) && def_starts &&
Expand Down Expand Up @@ -1588,8 +1588,8 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
int def_count = 0;
CBMLSPDef *defs = NULL;
if (def_modules && def_starts) {
defs = cbm_pxc_collect_all_defs(cache, probe_files, probe_count, project, def_modules,
&def_count, def_starts);
defs = cbm_pxc_collect_all_defs(NULL, cache, probe_files, probe_count, project,
def_modules, &def_count, def_starts);
rc = cbm_lsp_surface_build_rows(project, cache, probe_files, probe_count, defs,
def_starts, out_rows, out_count);
} else {
Expand Down
Loading
Loading