Skip to content

Commit 1189c87

Browse files
authored
Merge pull request #1908 from DeusData/feat/lsp-cross-inheritance
feat(lsp): resolve cross-file base classes in the Python/TS cross-LSP
2 parents a272663 + 2910e28 commit 1189c87

6 files changed

Lines changed: 295 additions & 45 deletions

File tree

‎src/pipeline/pass_lsp_cross.c‎

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,114 @@ static const char *pxc_join_pipe(CBMArena *arena, const char *const *items) {
150150
return buf;
151151
}
152152

153+
/* ── Cross-file base-class QN resolution ──────────────────────────
154+
*
155+
* CBMDefinition.base_classes carries the SOURCE SPELLING of each base
156+
* ("Base", "django.db.Model", "React.Component"): extraction strips
157+
* keywords and generic arguments, but it cannot know WHERE the name is
158+
* declared. The Python and TS cross-file registrars, however, consume
159+
* CBMLSPDef.embedded_types as fully-qualified names — py_lookup_attribute
160+
* and ts_lookup_member feed each entry straight into
161+
* cbm_registry_lookup_type. An unqualified spelling therefore matched
162+
* nothing declared in ANOTHER file: `class Child(Base)` in child.py never
163+
* saw Base in base.py, so a call to an inherited method through a typed
164+
* receiver had no member to find and fell through to the weak textual
165+
* cascade (where the #592/#606 guard correctly kills it).
166+
*
167+
* Resolve each base name ONCE per definition here, from exactly the
168+
* inputs pass_semantic uses to draw its INHERITS edge: the project
169+
* registry, the declaring module, and the file's import map. Two
170+
* properties follow. The LSP's inheritance view is the same relation the
171+
* graph records, so the two cannot diverge. And the binding is
172+
* import- or same-module-backed, not a short-name guess, so the CALLS
173+
* edge it enables is a supported fact that the weak-member guard keeps.
174+
*
175+
* Cost: O(defs) hash lookups + ONE import map per file. No per-call-site
176+
* hierarchy walk, no registry scan, no per-file registry rebuild.
177+
*/
178+
static bool pxc_lang_resolves_base_qns(CBMLanguage lang) {
179+
switch (lang) {
180+
case CBM_LANG_PYTHON:
181+
case CBM_LANG_JAVASCRIPT:
182+
case CBM_LANG_TYPESCRIPT:
183+
case CBM_LANG_TSX:
184+
return true;
185+
default:
186+
/* Go / JVM / C# / C++ / Rust registrars qualify their own embedded
187+
* types already (struct embedding, parent_class chains, impl-trait
188+
* provenance). Re-resolving here would fight those paths, so they
189+
* keep the raw join. */
190+
return false;
191+
}
192+
}
193+
194+
/* True for the registry strategies that are pure short-name guesses.
195+
* EXPLICIT drop-list, mirroring cbm_tsjs_suppress_weak_method_match: a
196+
* base class bound by "some project type happens to share this name" is
197+
* exactly the fabricated relation #606 removed, and inheritance
198+
* multiplies it — every inherited member of the wrong base would become
199+
* a callable target. Import-, module- and suffix-aware strategies are
200+
* kept; anything unresolved simply retains its source spelling and the
201+
* behaviour that predates this resolution. */
202+
static bool pxc_base_strategy_is_weak(const char *strategy) {
203+
if (!strategy || !strategy[0]) {
204+
return true;
205+
}
206+
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
207+
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
208+
}
209+
210+
/* Resolve one base-class spelling to a project QN. Mirrors
211+
* pass_semantic.c::resolve_as_class — same registry, same type-like veto —
212+
* then additionally rejects weak short-name strategies (see above).
213+
* Returns NULL when the base is not a confidently-known project type;
214+
* stdlib and third-party bases land here and keep their raw spelling. */
215+
static const char *pxc_resolve_base_qn(const cbm_registry_t *reg, const char *raw,
216+
const char *module_qn, const char **imp_keys,
217+
const char **imp_vals, int imp_count) {
218+
if (!reg || !raw || !raw[0]) {
219+
return NULL;
220+
}
221+
cbm_resolution_t res = cbm_registry_resolve(reg, raw, module_qn, imp_keys, imp_vals, imp_count);
222+
if (!res.qualified_name || !res.qualified_name[0]) {
223+
return NULL;
224+
}
225+
if (pxc_base_strategy_is_weak(res.strategy)) {
226+
return NULL;
227+
}
228+
if (!cbm_label_is_type_like(cbm_registry_label_of(reg, res.qualified_name))) {
229+
return NULL;
230+
}
231+
return res.qualified_name;
232+
}
233+
234+
/* pxc_join_pipe over base_classes, substituting each resolved QN for its
235+
* source spelling. Unresolved entries pass through verbatim so a base the
236+
* registry does not know keeps working exactly as before. */
237+
static const char *pxc_join_base_qns(CBMArena *arena, const char *const *bases,
238+
const cbm_registry_t *reg, const char *module_qn,
239+
const char **imp_keys, const char **imp_vals, int imp_count) {
240+
if (!bases || !bases[0]) {
241+
return NULL;
242+
}
243+
int count = 0;
244+
while (bases[count]) {
245+
count++;
246+
}
247+
const char **resolved =
248+
(const char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const char *));
249+
if (!resolved) {
250+
return pxc_join_pipe(arena, bases);
251+
}
252+
for (int i = 0; i < count; i++) {
253+
const char *qn =
254+
pxc_resolve_base_qn(reg, bases[i], module_qn, imp_keys, imp_vals, imp_count);
255+
resolved[i] = qn ? qn : bases[i];
256+
}
257+
resolved[count] = NULL;
258+
return pxc_join_pipe(arena, resolved);
259+
}
260+
153261
static bool pxc_is_jvm_lang(CBMLanguage lang);
154262

155263
static const char *pxc_last_component(const char *qn) {
@@ -262,7 +370,9 @@ static const char *pxc_qn_leaf(const char *name) {
262370
* to skip (unsupported label or missing required field). dst gets borrowed
263371
* pointers into src and into `arena` for synthesised composites. */
264372
static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const char *module_qn,
265-
const char *namespace_name, CBMLanguage lang, CBMLSPDef *dst) {
373+
const char *namespace_name, CBMLanguage lang, CBMLSPDef *dst,
374+
const cbm_registry_t *reg, const char **imp_keys,
375+
const char **imp_vals, int imp_count) {
266376
const char *label = pxc_map_label(src->label);
267377
if (!label || !src->qualified_name || !src->name)
268378
return -1;
@@ -283,7 +393,13 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
283393
* for multi-return languages (Go); single-return languages just see one
284394
* piece, which is what's already stored. */
285395
dst->return_types = src->return_type;
286-
dst->embedded_types = pxc_join_pipe(arena, src->base_classes);
396+
/* Languages whose cross registrars read embedded_types as QNs get their
397+
* bases resolved against the project registry; everyone else keeps the
398+
* raw source spelling their own registrar already knows how to handle. */
399+
dst->embedded_types = (reg && pxc_lang_resolves_base_qns(lang))
400+
? pxc_join_base_qns(arena, src->base_classes, reg, module_qn,
401+
imp_keys, imp_vals, imp_count)
402+
: pxc_join_pipe(arena, src->base_classes);
287403
dst->signature_param_types = src->signature_param_types;
288404
dst->signature_param_count = src->signature_param_count;
289405
dst->lang = lang;
@@ -323,9 +439,10 @@ static int pxc_build_rust_impl_relation(CBMArena *arena, const CBMImplTrait *imp
323439
/* Collect a project-wide CBMLSPDef[] from all cached results. Returns a
324440
* malloc'd array (caller frees) of length *out_count. String fields are
325441
* borrowed from cache[i]->arena and from def_modules[i] (also borrowed). */
326-
CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t *files,
327-
int file_count, const char *project_name, char **def_modules,
328-
int *out_count, int *out_def_starts) {
442+
CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
443+
const cbm_file_info_t *files, int file_count,
444+
const char *project_name, char **def_modules, int *out_count,
445+
int *out_def_starts) {
329446
int total = 0;
330447
for (int i = 0; i < file_count; i++) {
331448
if (cache[i]) {
@@ -369,12 +486,30 @@ CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t
369486
cache[fi]->namespace_name = namespace_name;
370487
}
371488
}
489+
/* One import map per FILE (not per def, and not per base name): the
490+
* cross-file base-class resolution below needs the same local-name →
491+
* import-QN view pass_semantic uses. Built only for the languages
492+
* that consume resolved base QNs, and only when a caller supplied the
493+
* pipeline context (the surface-probe path passes NULL and keeps the
494+
* raw spelling). */
495+
const cbm_registry_t *base_reg = NULL;
496+
const char **imp_keys = NULL;
497+
const char **imp_vals = NULL;
498+
int imp_count = 0;
499+
if (ctx && ctx->registry && pxc_lang_resolves_base_qns(files[fi].language)) {
500+
base_reg = ctx->registry;
501+
cbm_pxc_build_import_map(ctx->gbuf, project_name, files[fi].rel_path,
502+
files[fi].language, cache[fi], &imp_keys, &imp_vals,
503+
&imp_count);
504+
}
372505
for (int di = 0; di < cache[fi]->defs.count; di++) {
373506
if (pxc_build_lsp_def(&cache[fi]->arena, &cache[fi]->defs.items[di], def_modules[fi],
374-
namespace_name, files[fi].language, &defs[idx]) == 0) {
507+
namespace_name, files[fi].language, &defs[idx], base_reg,
508+
imp_keys, imp_vals, imp_count) == 0) {
375509
idx++;
376510
}
377511
}
512+
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
378513
if (files[fi].language == CBM_LANG_RUST) {
379514
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
380515
if (pxc_build_rust_impl_relation(
@@ -1284,7 +1419,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
12841419

12851420
int def_count = 0;
12861421
int *def_starts = (int *)calloc((size_t)file_count + 1, sizeof(int));
1287-
CBMLSPDef *all_defs = cbm_pxc_collect_all_defs(cache, files, file_count, ctx->project_name,
1422+
CBMLSPDef *all_defs = cbm_pxc_collect_all_defs(ctx, cache, files, file_count, ctx->project_name,
12881423
def_modules, &def_count, def_starts);
12891424
/* Same seam as the parallel driver: serialize per-file surfaces while the
12901425
* result cache is alive. Failure only degrades to a full rebuild on the

‎src/pipeline/pass_lsp_cross.h‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,19 @@ bool cbm_pxc_has_cross_lsp(CBMLanguage lang);
5454
* receives per-file prefix offsets: file i's defs occupy
5555
* [out_def_starts[i], out_def_starts[i+1]) — the LSP-surface serializer
5656
* needs the per-file slices, which the flat array does not otherwise
57-
* record. */
58-
CBMLSPDef *cbm_pxc_collect_all_defs(CBMFileResult **cache, const cbm_file_info_t *files,
59-
int file_count, const char *project_name, char **def_modules,
60-
int *out_count, int *out_def_starts);
57+
* record.
58+
*
59+
* `ctx` (nullable) enables cross-file base-class resolution: for the
60+
* languages whose cross registrars read embedded_types as qualified names
61+
* (Python, JS/TS/TSX), every CBMDefinition.base_classes spelling is resolved
62+
* to a project QN through ctx->registry plus the file's import map — the same
63+
* inputs pass_semantic uses to draw its INHERITS edge, so the LSP's
64+
* inheritance view and the graph's cannot diverge. Pass NULL to keep the raw
65+
* source spelling (surface-probe paths that build no registry). */
66+
CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
67+
const cbm_file_info_t *files, int file_count,
68+
const char *project_name, char **def_modules, int *out_count,
69+
int *out_def_starts);
6170

6271
/* Detect TS dialect flags from a relative path. */
6372
void cbm_pxc_ts_modes(CBMLanguage lang, const char *rel_path, bool *out_js, bool *out_jsx,

‎src/pipeline/pipeline.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,7 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
12531253
def_modules = (char **)calloc((size_t)file_count, sizeof(char *));
12541254
def_starts = (int *)calloc((size_t)file_count + 1, sizeof(int));
12551255
all_defs = def_modules
1256-
? cbm_pxc_collect_all_defs(cache, files, file_count, ctx->project_name,
1256+
? cbm_pxc_collect_all_defs(ctx, cache, files, file_count, ctx->project_name,
12571257
def_modules, &def_count, def_starts)
12581258
: NULL;
12591259
}

‎src/pipeline/pipeline_incremental.c‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,7 +1285,7 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
12851285
int fresh_count = 0;
12861286
CBMLSPDef *fresh_defs =
12871287
def_modules && def_starts
1288-
? cbm_pxc_collect_all_defs(cache, changed_files, ci, ctx->project_name,
1288+
? cbm_pxc_collect_all_defs(ctx, cache, changed_files, ci, ctx->project_name,
12891289
def_modules, &fresh_count, def_starts)
12901290
: NULL;
12911291
if ((fresh_defs || fresh_count == 0) && def_starts &&
@@ -1588,8 +1588,8 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
15881588
int def_count = 0;
15891589
CBMLSPDef *defs = NULL;
15901590
if (def_modules && def_starts) {
1591-
defs = cbm_pxc_collect_all_defs(cache, probe_files, probe_count, project, def_modules,
1592-
&def_count, def_starts);
1591+
defs = cbm_pxc_collect_all_defs(NULL, cache, probe_files, probe_count, project,
1592+
def_modules, &def_count, def_starts);
15931593
rc = cbm_lsp_surface_build_rows(project, cache, probe_files, probe_count, defs,
15941594
def_starts, out_rows, out_count);
15951595
} else {

0 commit comments

Comments
 (0)