/* * repro_ts_inherited_method.c — RED reproduction: TypeScript CROSS-FILE * INHERITED method call resolution gap (ts_lsp). * * THE GAP: a typed call to a method the receiver's class INHERITS from a base * class defined in ANOTHER file is never resolved by the TS LSP: * * base.ts: export class Base { greet(): string { ... } } * derived.ts: import { Base } from "./base"; * export class Derived extends Base { ... } * export function callSite(): string { * const d: Derived = new Derived(); * return d.greet(); <-- inherited, cross-file * } * * CORRECT behaviour (asserted here, so this test is RED until fixed): a CALLS * edge from the caller (callable-sourced, QN contains "callSite") to the BASE * method definition (target QN suffix ".Base.greet" — mirroring the * java/kotlin/php inherited-dispatch convention: resolution lands on the base * class's method def, since Derived declares no `greet` and no such node can * exist), carrying a genuine TS-LSP resolution strategy ("lsp_ts_*" in the * edge's properties_json, per the ts_emit_resolved_call contract documented in * repro_lsp_ts.c). * * WHY the strategy tag is part of the invariant: before the weak-short-name * suppression (PR #840, recovering withdrawn #836/#835), this scenario looked * resolved via a unique_name REGISTRY fallback — "greet" is unique in a 2-file * fixture, so a short-name guess happened to bind the right node (a false * green; in a real repo the same guess binds an arbitrary same-named method). * PR #840 flipped the probe lrp_ts_s6_inherited_method * (tests/test_lsp_resolution_probe.c) to assert that this weak edge is * SUPPRESSED (calls == 0) — correct, but it leaves the underlying resolution * gap without any red reproduction. THIS test is that reproduction: * - on pre-#840 code the lucky edge exists but carries NO "lsp_ts" strategy * -> RED (the green was never genuine); * - on post-#840 code the weak edge is suppressed, no edge exists at all * -> RED; * - only genuine ts_lsp cross-file inheritance resolution turns it GREEN. * * ROOT-CAUSE POINTER (for the eventual fixer): ts_lsp cross-file inheritance * resolution — internal/cbm/lsp/ts_lsp.c resolve_member_call/lookup_method * only walks methods declared on the receiver's OWN class as registered in the * module registry; it does not follow the (correctly extracted) INHERITS edge * from Derived to an imported Base to find `greet` there. See PR #836/#840 and * the S6 probe lrp_ts_s6_inherited_method for the full analysis. The INHERITS * edge and both defs ARE in the graph (asserted below as preconditions), so a * red here is the RESOLUTION gap, not an extraction failure. * * NOTE: line comments only inside this header (no nested block comments, per * coding rules). */ #include "test_framework.h" #include "repro_invariant_lib.h" #include #include /* ── Fixture ─────────────────────────────────────────────────────────────── */ static const RFile kTsInherited[] = { {"base.ts", "export class Base {\n" " greet(): string { return \"hello\"; }\n" "}\n"}, {"derived.ts", "import { Base } from \"./base\";\n" "\n" "export class Derived extends Base {\n" " extra(): number { return 2; }\n" "}\n" "\n" "export function callSite(): string {\n" " const d: Derived = new Derived();\n" " return d.greet();\n" "}\n"}, }; /* ── Local store helpers ─────────────────────────────────────────────────── */ /* True if some node with `label` has a QN ending in `suffix`. */ static int node_with_qn_suffix(cbm_store_t *store, const char *project, const char *label, const char *suffix) { cbm_node_t *nodes = NULL; int count = 0; if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) return 0; int found = 0; size_t sl = strlen(suffix); for (int i = 0; i < count && !found; i++) { const char *qn = nodes[i].qualified_name; if (qn) { size_t ql = strlen(qn); if (ql >= sl && strcmp(qn + ql - sl, suffix) == 0) found = 1; } } cbm_store_free_nodes(nodes, count); return found; } /* * The PRIMARY invariant, checked on a SINGLE edge (independent per-edge checks * could false-green by combining a strategy-less lucky edge with an unrelated * lsp_ts-tagged edge): there exists a CALLS edge whose * - source is callable-sourced (Function/Method) and its QN contains * `caller_substr`; * - target QN ends with `callee_suffix`; * - properties_json carries `strategy_substr` (substring, so any concrete * "lsp_ts_*" tag satisfies "lsp_ts"). * When `dump` is non-zero every CALLS edge is printed to stderr so a RED run * documents exactly what the graph contains instead. */ static int lsp_resolved_edge_exists(cbm_store_t *store, const char *project, const char *caller_substr, const char *callee_suffix, const char *strategy_substr, int dump) { cbm_edge_t *edges = NULL; int n = 0; if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) != CBM_STORE_OK) return 0; int found = 0; size_t cl = strlen(callee_suffix); for (int i = 0; i < n; i++) { cbm_node_t src, tgt; if (cbm_store_find_node_by_id(store, edges[i].source_id, &src) != CBM_STORE_OK) continue; if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK) continue; if (dump) fprintf(stderr, " [ts-inherited] CALLS %s (%s) -> %s props=%s\n", src.qualified_name ? src.qualified_name : "?", src.label ? src.label : "?", tgt.qualified_name ? tgt.qualified_name : "?", edges[i].properties_json ? edges[i].properties_json : "{}"); const char *slabel = src.label ? src.label : ""; if (strcmp(slabel, "Function") != 0 && strcmp(slabel, "Method") != 0) continue; if (!src.qualified_name || !strstr(src.qualified_name, caller_substr)) continue; const char *tqn = tgt.qualified_name; if (!tqn) continue; size_t tl = strlen(tqn); if (tl < cl || strcmp(tqn + tl - cl, callee_suffix) != 0) continue; if (!edges[i].properties_json || !strstr(edges[i].properties_json, strategy_substr)) continue; found = 1; } cbm_store_free_edges(edges, n); return found; } /* ── Tests ───────────────────────────────────────────────────────────────── */ /* * Extraction-tier preconditions — expected GREEN on current code. These prove * a red in the pipeline test below is the RESOLUTION gap, not a fixture or * extraction error: both files parse without has_error, base.ts yields the * Method def `greet`, and derived.ts yields the `greet` call site. */ TEST(repro_ts_inherited_extraction_preconditions) { /* base.ts extracts cleanly and defines Method greet. */ ASSERT_TRUE(inv_extract_clean(kTsInherited[0].content, CBM_LANG_TYPESCRIPT, "base.ts")); CBMFileResult *rb = inv_rx(kTsInherited[0].content, CBM_LANG_TYPESCRIPT, "base.ts"); ASSERT_NOT_NULL(rb); int greet_methods = 0; for (int i = 0; i < rb->defs.count; i++) { CBMDefinition *d = &rb->defs.items[i]; if (d->label && strcmp(d->label, "Method") == 0 && d->name && strcmp(d->name, "greet") == 0) greet_methods++; } cbm_free_result(rb); ASSERT_EQ(greet_methods, 1); /* derived.ts extracts cleanly and contains the greet call site. */ ASSERT_TRUE(inv_extract_clean(kTsInherited[1].content, CBM_LANG_TYPESCRIPT, "derived.ts")); CBMFileResult *rd = inv_rx(kTsInherited[1].content, CBM_LANG_TYPESCRIPT, "derived.ts"); ASSERT_NOT_NULL(rd); int has_greet_call = inv_has_call(rd, "greet"); cbm_free_result(rd); ASSERT_TRUE(has_greet_call); PASS(); } /* * THE RED REPRODUCTION — index the 2-file fixture through the full production * pipeline and assert the CORRECT outcome: an LSP-resolved CALLS edge from * callSite to Base.greet. Store-level preconditions (callee node present, * INHERITS extracted) are checked first so the failure is attributable to the * missing ts_lsp cross-file inheritance resolution and nothing else. * Returns 0 on PASS (gap fixed), non-zero on FAIL (RED = the open gap). */ static int run_ts_inherited_pipeline(void) { RProj lp; cbm_store_t *store = rh_index_files( &lp, kTsInherited, (int)(sizeof(kTsInherited) / sizeof(kTsInherited[0]))); if (!store) { printf(" %sFAIL%s %s:%d: index failed (setup, NOT the gap)\n", tf_red(), tf_reset(), __FILE__, __LINE__); rh_cleanup(&lp, store); return 1; } int rc = 0; /* Precondition: the callee def node exists in the graph. */ if (!node_with_qn_suffix(store, lp.project, "Method", ".Base.greet")) { printf(" %sFAIL%s %s:%d: precondition — no Method node with QN suffix " "\".Base.greet\" (extraction problem, NOT the resolution gap)\n", tf_red(), tf_reset(), __FILE__, __LINE__); rc = 1; } /* Precondition: `Derived extends Base` produced an INHERITS edge (the S6 * probe diagnostic confirms extraction emits it; the gap is downstream). */ int inherits = rh_count_edges(store, lp.project, "INHERITS"); if (inherits < 1) { printf(" %sFAIL%s %s:%d: precondition — INHERITS=%d (expected >=1; " "extraction problem, NOT the resolution gap)\n", tf_red(), tf_reset(), __FILE__, __LINE__, inherits); rc = 1; } /* PRIMARY: the inherited call is resolved BY THE TS LSP — a CALLS edge * callSite -> *.Base.greet carrying an "lsp_ts" strategy. A short-name * registry fallback edge (no lsp_ts tag) does NOT satisfy this; nor does * post-#840 suppression (no edge at all). */ if (!lsp_resolved_edge_exists(store, lp.project, "callSite", ".Base.greet", "lsp_ts", 0)) { /* Dump what the graph actually contains so the RED row documents it. */ (void)lsp_resolved_edge_exists(store, lp.project, "callSite", ".Base.greet", "lsp_ts", 1); printf(" %sFAIL%s %s:%d: no lsp_ts-resolved CALLS edge callSite -> " "*.Base.greet — TS cross-file INHERITED method call is " "UNRESOLVED (ts_lsp inheritance gap, see #836/#840)\n", tf_red(), tf_reset(), __FILE__, __LINE__); rc = 1; } rh_cleanup(&lp, store); return rc; } TEST(repro_ts_inherited_method_call_resolution) { return run_ts_inherited_pipeline(); } /* ── Suite ───────────────────────────────────────────────────────────────── */ SUITE(repro_ts_inherited_method) { RUN_TEST(repro_ts_inherited_extraction_preconditions); RUN_TEST(repro_ts_inherited_method_call_resolution); }