diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 684554455..ab92301e8 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -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) { @@ -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; @@ -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; @@ -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]) { @@ -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( @@ -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 diff --git a/src/pipeline/pass_lsp_cross.h b/src/pipeline/pass_lsp_cross.h index 7630d8c72..705b804cc 100644 --- a/src/pipeline/pass_lsp_cross.h +++ b/src/pipeline/pass_lsp_cross.h @@ -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, diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index cc9190f52..938437549 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -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; } diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 4ff89aa42..bf71a1f84 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -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 && @@ -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 { diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index 7386710f9..91844f67e 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -222,6 +222,36 @@ static void lrp_diag(cbm_store_t *store, const char *project, const char *scenar fprintf(stderr, " [LRP] %s edges=[%s]\n", scenario, line[0] ? line : "(none)"); } +/* Count CALLS edges whose resolution strategy starts with `prefix`. + * + * The S6 inheritance scenarios need more than "an edge exists": a fixture + * small enough to have exactly one same-named method can be satisfied by a + * weak short-name guess that happens to be right, which is precisely the + * class of edge the receiver-aware guard (#592/#606) removes. Asserting an + * `lsp_*` strategy is what distinguishes inheritance RESOLUTION from a lucky + * coincidence. Returns -1 when the edges cannot be read at all. */ +static int lrp_count_calls_with_strategy(cbm_store_t *store, const char *project, + const char *prefix) { + if (!store) { + return -1; + } + cbm_edge_t *edges = NULL; + int n = 0; + if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) != CBM_STORE_OK) { + return -1; + } + char needle[64]; + snprintf(needle, sizeof(needle), "\"strategy\":\"%s", prefix); + int hits = 0; + for (int i = 0; i < n; i++) { + if (edges[i].properties_json && strstr(edges[i].properties_json, needle)) { + hits++; + } + } + cbm_store_free_edges(edges, n); + return hits; +} + /* Index + assert CALLS >= floor; on failure emit diagnostics. * expect_green=true: ASSERT_TRUE (green guard). * expect_green=false: still asserts CALLS>=1 (the correct outcome), so RED @@ -952,21 +982,28 @@ TEST(lrp_python_s6_inherited_method) { {"child.py", "from .base import Base\n\n\nclass Child(Base):\n" " def extra(self):\n return 'extra'\n\n\n" "def run(c):\n return c.describe()\n"}}; - /* RED: c.describe() needs py_lsp_cross to walk `class Child(Base)`. Exactly - * the TS/S6 situation above, and resolved the same way (#1276). - * Until the receiver-aware guard landed, this scenario passed via a - * unique_name registry fallback — MEASURED on main before the guard: - * strategy=unique_name, cands=1, conf=0.7500. "describe" is the only symbol - * of that name in this 2-file fixture, so a weak short-name guess happened - * to be right. In a real repo the same guess binds an arbitrary same-named - * method (the false edges #1276 targets: accelerator.print() -> - * MockAccelerator.print), so the guard now suppresses weak member-call - * matches whose receiver is unresolved — here `c`, a bare parameter. - * c.describe() is the ONLY call in the fixture, so a correctly-suppressed - * run yields exactly zero CALLS. + /* MEASURED on main before the guard: strategy=unique_name, cands=1, + * conf=0.7500. "describe" is the only symbol of that name in this 2-file + * fixture, so a weak short-name guess happened to be right. In a real repo + * the same guess binds an arbitrary same-named method (the false edges + * #1276 targets: accelerator.print() -> MockAccelerator.print), so the + * Python weak-member guard suppresses member calls whose receiver is + * unresolved -- here `c`, a bare parameter. c.describe() is the ONLY call + * in the fixture, so a correctly-suppressed run yields exactly zero CALLS. + * + * This is NOT an inheritance case, despite the name, and it has no + * flip-back condition: `c` is un-annotated, so no receiver type exists to + * inherit THROUGH. Resolving cross-file base classes cannot recover this + * edge, because there is nothing to say `c` is a Child. Cross-file + * inheritance resolution is covered by S6b below, which supplies the + * receiver type this fixture deliberately withholds. + * + * The base_classes extraction bug this comment used to cite is FIXED -- + * extraction_inheritance is green for Python, and base_classes holds clean + * names with parens, kwargs and generic subscripts already stripped. + * * Tripwire: assert the store opened AND calls == 0 exactly, so an infra/DB - * failure cannot pass vacuously; flip to ASSERT calls >= 1 once py_lsp_cross - * resolves inheritance (lsp_py_*, a strategy the guard keeps). */ + * failure cannot pass vacuously. */ LRP_Proj lp; cbm_store_t *store = lrp_index(&lp, f, 2); ASSERT_NOT_NULL(store); @@ -976,6 +1013,61 @@ TEST(lrp_python_s6_inherited_method) { PASS(); } +/* S6b — Python inherited method call through a receiver whose type IS known. + * + * S6 above is deliberately kept as-is, but it cannot demonstrate inheritance + * resolution: its `def run(c)` parameter carries no annotation, so nothing in + * the program says `c` is a Child. Whatever binds c.describe() there is a + * guess about a name, not a fact about a type — which is why it resolves via + * unique_name and why the Python weak-member guard (#1324) is expected to take + * it to zero. + * + * This row supplies the receiver type three ways — annotated parameter, `self` + * inside the subclass, and a constructor result — so `Base.describe` is + * reachable ONLY by crossing the file boundary that `class Child(Base)` + * declares. Before cbm_pxc_collect_all_defs resolved base spellings to project + * QNs, py_lsp_cross received "Base" and looked it up as a qualified name, found + * nothing (Base lives in base.py), and all three fell through to unique_name at + * confidence 0.75. Asserting the lsp_* strategy is what separates the two + * outcomes: a short-name guess and a resolved inherited member produce the same + * edge COUNT here, and only the strategy tells them apart. */ +TEST(lrp_python_s6b_inherited_method_typed_receiver) { + static const LRP_File annotated[] = { + {"base.py", "class Base:\n def describe(self):\n return 'base'\n"}, + {"child.py", "from .base import Base\n\n\nclass Child(Base):\n" + " def extra(self):\n return 'extra'\n\n\n" + "def run(c: Child):\n return c.describe()\n"}}; + static const LRP_File self_attr[] = { + {"base.py", "class Base:\n def describe(self):\n return 'base'\n"}, + {"child.py", "from .base import Base\n\n\nclass Child(Base):\n" + " def go(self):\n return self.describe()\n"}}; + static const LRP_File constructed[] = { + {"base.py", "class Base:\n def describe(self):\n return 'base'\n"}, + {"child.py", "from .base import Base\n\n\nclass Child(Base):\n pass\n\n\n" + "def run():\n return Child().describe()\n"}}; + const LRP_File *shapes[3] = {annotated, self_attr, constructed}; + const char *names[3] = {"annotated_param", "self_attr", "constructor"}; + + for (int k = 0; k < 3; k++) { + LRP_Proj lp; + cbm_store_t *store = lrp_index(&lp, shapes[k], 2); + ASSERT_NOT_NULL(store); + int inherits = cbm_store_count_edges_by_type(store, lp.project, "INHERITS"); + int lsp_calls = lrp_count_calls_with_strategy(store, lp.project, "lsp_"); + if (inherits < 1 || lsp_calls < 1) { + fprintf(stderr, " [LRP] python/S6b/%s FAIL inherits=%d lsp_calls=%d\n", names[k], + inherits, lsp_calls); + lrp_diag(store, lp.project, names[k]); + } + lrp_cleanup(&lp, store); + /* INHERITS proves the cross-file base really was resolved; the lsp_ + * strategy proves the CALL rode that relation instead of guessing. */ + ASSERT_TRUE(inherits >= 1); + ASSERT_TRUE(lsp_calls >= 1); + } + PASS(); +} + /* S7 — Python type-annotated call (PEP 484 type hint). */ TEST(lrp_python_s7_annotated_call) { static const LRP_File f[] = { @@ -1116,25 +1208,38 @@ TEST(lrp_ts_s6_inherited_method) { "import { Base } from './base';\n\n" "export class Child extends Base {\n extra(): string { return 'child'; }\n}\n\n" "export function run(c: Child): string { return c.describe(); }\n"}}; - /* RED: c.describe() needs ts_lsp_cross to walk `Child extends Base`. The - * INHERITS edge IS extracted (the probe diagnostic shows INHERITS=1); the - * gap is in ts_lsp_cross's cross-file inheritance RESOLUTION, not extraction. - * Until the receiver-aware guard (#592/#606) landed, this scenario passed via - * a unique_name registry fallback — "describe" is unique in this 2-file - * fixture, so a weak short-name guess happened to be right. In a real repo the - * same guess binds an arbitrary same-named method (the false edges #606 - * targets), so the guard now suppresses weak member-call matches with an - * unresolved receiver. c.describe() is the ONLY call in the fixture, so a - * correctly-suppressed run yields exactly zero CALLS. - * Tripwire: assert store opened AND calls == 0 exactly (an infra/DB failure - * must not vacuously pass); flip to ASSERT calls >= 1 once ts_lsp_cross - * resolves inheritance (lsp_ts_*, a strategy the guard keeps). */ + /* GREEN: c.describe() resolves through `Child extends Base` across files. + * + * This scenario was RED for as long as ts_lsp_cross received the base class + * as its SOURCE SPELLING ("Base") while ts_lookup_member fed embedded_types + * straight into cbm_registry_lookup_type as a QN — so a base declared in + * ANOTHER file never matched, the receiver stayed unresolved, and the only + * call in the fixture died in the weak textual cascade. Its predecessor + * "pass" came from a unique_name registry fallback ("describe" is unique + * here, so a weak short-name guess happened to be right); the receiver-aware + * guard (#592/#606) correctly suppressed that, which is why the tripwire + * asserted exactly zero. + * + * cbm_pxc_collect_all_defs now resolves each base spelling to a project QN + * through the import map + project registry — the same inputs pass_semantic + * uses for its INHERITS edge — so `Child extends Base` is a cross-file fact + * the TS LSP can walk. Asserting the lsp_ts_* strategy is the whole point: + * it proves the edge came from inheritance RESOLUTION and not from another + * lucky short-name guess. Keep BOTH assertions — calls>=1 alone would go + * green again for the wrong reason if the guard ever regressed. */ LRP_Proj lp; cbm_store_t *store = lrp_index(&lp, f, 2); ASSERT_NOT_NULL(store); int calls = cbm_store_count_edges_by_type(store, lp.project, "CALLS"); + int lsp_calls = lrp_count_calls_with_strategy(store, lp.project, "lsp_ts"); + if (calls < 1 || lsp_calls < 1) { + fprintf(stderr, " [LRP] ts/S6/inherited_method FAIL calls=%d lsp_ts_calls=%d\n", calls, + lsp_calls); + lrp_diag(store, lp.project, "ts/S6/inherited_method"); + } lrp_cleanup(&lp, store); - ASSERT_EQ(calls, 0); + ASSERT_TRUE(calls >= 1); + ASSERT_TRUE(lsp_calls >= 1); PASS(); } @@ -1800,6 +1905,7 @@ SUITE(lsp_resolution_probe) { RUN_TEST(lrp_python_s4_class_method); RUN_TEST(lrp_python_s5_chained); RUN_TEST(lrp_python_s6_inherited_method); + RUN_TEST(lrp_python_s6b_inherited_method_typed_receiver); RUN_TEST(lrp_python_s7_annotated_call); RUN_TEST(lrp_python_s8_field_type_hint); RUN_TEST(lrp_python_crossfile_dunder_carrier); diff --git a/tests/test_parallel.c b/tests/test_parallel.c index 2b94130e5..181c1f7ca 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -324,8 +324,8 @@ static cbm_gbuf_t *run_parallel_with_extract_opts_and_mutator( char **def_modules = (char **)calloc((size_t)file_count, sizeof(char *)); int def_count = 0; CBMLSPDef *all_defs = - def_modules ? cbm_pxc_collect_all_defs(result_cache, files, file_count, ctx.project_name, - def_modules, &def_count, NULL) + def_modules ? cbm_pxc_collect_all_defs(&ctx, result_cache, files, file_count, + ctx.project_name, def_modules, &def_count, NULL) : NULL; CBMModuleDefIndex *module_def_index = all_defs ? cbm_pxc_build_module_def_index(all_defs, def_count) : NULL;