/* * repro_lsp_ts.c — EXHAUSTIVE per-LSP-pass invariant suite for the TypeScript / * JavaScript / JSX hybrid LSP (internal/cbm/lsp/ts_lsp.c). * * WHAT THIS ASSERTS — the LSP RESOLUTION CONTRACT, one invariant per strategy. * The TS cross resolver resolves each call via a specific STRATEGY and tags the * resulting CALLS edge in its properties_json with * "strategy":"lsp_" * (see ts_emit_resolved_call, ts_lsp.c:109-120; every concrete emit site passes * a literal "lsp_ts..." string). Each strategy keys on a precise TS/TSX * construct. This suite builds the MINIMAL fixture that exercises exactly one * strategy, indexes it through the full production pipeline, and asserts TWO * things: * (a) callable-sourcing — the inner call is sourced at a Function/Method * node, never at a Module/File node (inv_count_calls_by_source → * module_sourced == 0). A Module-sourced call is the #554 attribution * bug; this is the broad correctness floor. * (b) strategy-presence — some CALLS edge carries "lsp_" in its * properties_json (inv_edge_has_strategy). This is the PRECISE per-pass * invariant: it proves that exact resolution path fired and survived into * the graph. * * RED vs GREEN — this is a STATUS BOARD, not a pass/fail gate (runs only under * make test-repro / bug-repro.yml, never the branch-protection ci-ok gate): * - GREEN = the LSP strategy works end-to-end = a permanent regression * guard that it keeps working. * - RED = the strategy is dropped, or the call lands Module-sourced, or * the rescue is discarded. Either way the per-pass TEST DOCUMENTS * the exact gap for the eventual fixer. * * TIE TO repro_invariant_lsp_rescue.c — that file pins the MECHANISM by which * these can silently fail: cbm_pipeline_find_lsp_resolution joins each * LSP-resolved call to the tree-sitter call by EXACT caller-QN string equality. * When tree-sitter's enclosing-func walk falls back to the MODULE QN but the * LSP built the real method QN, the strcmp never matches, the LSP rescue is * discarded, and the edge stays Module-sourced with a registry strategy — * NEVER an "lsp_" strategy. So a strategy that is correctly EMITTED by ts_lsp.c * can still be ABSENT from the graph here: the exact-QN join suppresses it. * Whenever a strategy below is RED, suspect that join first (a same-file * in-function fixture sidesteps it; a cross-file fixture exercises it). * * STRATEGY INVENTORY — every literal "lsp_..." emitted by ts_lsp.c, grepped from * the source (grep '"lsp_' internal/cbm/lsp/ts_lsp.c), with its keying site: * lsp_ts_local (ts_lsp.c:2322) bare identifier call f() resolving to a * module-local function (call_expression * function is an `identifier`, found in the * module registry). * lsp_ts_method (ts_lsp.c:2284) obj.method() type-based dispatch on a * receiver whose type is a NAMED in-file * class (member_expression, lookup_method * hits). * lsp_ts_namespace (ts_lsp.c:2246) Ns.fn() where Ns is a namespace import * (`import * as Ns from "./mod"`); the * member_expression object is an identifier * matching an import local name, fn resolves * in that module's registry. * lsp_ts_import (ts_lsp.c:2334) bare identifier call to an imported * function (`import { helper } ...`); the * identifier matches an import local name and * resolves in the imported module's registry. * lsp_ts_jsx (ts_lsp.c:2647) JSX element whose tag is a * module-local component function (TSX only; * uppercase tag, resolves via the module * registry). * lsp_ts_jsx_import (ts_lsp.c:2657) JSX element whose tag is an * imported component (TSX only; tag matches * an import local name → synthetic * "." QN). NOTE: this site * builds the callee QN WITHOUT verifying the * symbol exists in the registry, so it can * emit even when the import target is absent. * lsp_ts (ts_lsp.c:116) DEFAULT fallback inside ts_emit_resolved_call * used only when a caller passes a NULL * strategy. Every concrete emit site passes a * literal "lsp_ts..." string, so "lsp_ts" is * (as of this writing) never emitted as a * distinct tag — expected ABSENT (RED). This * TEST documents that the bare-"lsp_ts" path * has no live caller; if it ever goes GREEN a * new NULL-strategy emit site appeared. * lsp_unresolved (ts_lsp.c:128) fallback marker for an unresolved call * (ts_emit_unresolved_call, confidence 0.0). * A 0.0-confidence unresolved entry is * typically NOT promoted into a CALLS edge * with the strategy tag, so this is expected * ABSENT (RED) — it documents whether * "lsp_unresolved" surfaces in the graph. * * LANGUAGE SELECTION — the filename extension picks the language exactly as the * production indexer does: ".ts" → CBM_LANG_TYPESCRIPT, ".tsx" → CBM_LANG_TSX. * jsx_mode (required by resolve_jsx_element, ts_lsp.c:2620) is enabled ONLY for * CBM_LANG_TSX (cbm.c:619, pass_lsp_cross.c:267), so the two JSX fixtures use * ".tsx" files; the non-JSX fixtures use ".ts". * * 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 /* ── Shared per-strategy runners (DRY) ───────────────────────────────────── */ /* * assert_lsp_strategy_files * * Index an N-file fixture and assert the per-pass LSP RESOLUTION CONTRACT: * 1. the store opened (precondition — a setup failure is a FAIL, not a skip); * 2. callable-sourcing: NO CALLS edge is Module/File-sourced, and at least one * callable-sourced CALLS edge exists (else there is no signal at all); * 3. strategy-presence: some CALLS edge carries "lsp_" in its * properties_json. * * The filename extension selects the language exactly as the production indexer * does (".ts" → TypeScript, ".tsx" → TSX). Returns 0 on PASS (GREEN), non-zero * on FAIL (RED) — the redness is the documented per-pass status. */ static int assert_lsp_strategy_files(const RFile *files, int nfiles, const char *strategy) { RProj lp; cbm_store_t *store = rh_index_files(&lp, files, nfiles); if (!store) { printf(" %sFAIL%s %s:%d: index failed for strategy %s\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy); rh_cleanup(&lp, store); return 1; } int module_sourced = -1; int callable_sourced = -1; inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); int has_strategy = inv_edge_has_strategy(store, lp.project, strategy); int rc = 0; /* (a) callable-sourcing floor: zero Module/File-sourced CALLS edges. */ if (module_sourced != 0) { printf(" %sFAIL%s %s:%d: strategy %s: %d Module-sourced CALLS " "(expected 0)\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy, module_sourced); rc = 1; } /* There must be a callable-sourced CALLS edge, else the fixture produced no * call signal and the strategy assertion below would be vacuous. */ if (callable_sourced <= 0) { printf(" %sFAIL%s %s:%d: strategy %s: no callable-sourced CALLS edge " "(callable=%d)\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy, callable_sourced); rc = 1; } /* (b) the precise per-pass invariant: the resolution strategy is present. */ if (!has_strategy) { printf(" %sFAIL%s %s:%d: strategy %s ABSENT from any CALLS edge " "properties_json\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy); rc = 1; } rh_cleanup(&lp, store); return rc; } /* Single-file convenience wrapper. */ static int assert_lsp_strategy(const char *filename, const char *src, const char *strategy) { RFile f = {filename, src}; return assert_lsp_strategy_files(&f, 1, strategy); } /* * assert_no_resolvable_edge — the ACCURATE invariant for a call whose callee is * genuinely UNRESOLVABLE (undeclared symbol). No node can exist for it, so no * CALLS edge can ever form and no resolution strategy can land on an edge. Index * the single-file fixture and assert NO CALLS edge targets a node whose QN * contains `callee_substr`. Returns 0 on PASS, non-zero on FAIL. */ static int assert_no_resolvable_edge(const char *filename, const char *src, const char *callee_substr) { RProj lp; cbm_store_t *store = rh_index(&lp, filename, src); if (!store) { printf(" %sFAIL%s %s:%d: index failed for no-edge callee %s\n", tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); rh_cleanup(&lp, store); return 1; } int rc = 0; /* Exercised-check: the fixture MUST produce at least one callable-sourced * CALLS edge (its in-fixture control call). Without it the "no edge to * " invariant is VACUOUS — it also passes when extraction silently * produced nothing, so a green would not prove the unresolvable call was * actually processed and correctly dropped. */ int module_sourced = -1; int callable_sourced = -1; inv_count_calls_by_source(store, lp.project, &module_sourced, &callable_sourced); (void)module_sourced; if (callable_sourced <= 0) { printf(" %sFAIL%s %s:%d: no callable-sourced CALLS edge — fixture not " "exercised; the no-edge invariant for %s is vacuous\n", tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); rc = 1; } if (!inv_no_calls_edge_to_qn(store, lp.project, callee_substr)) { printf(" %sFAIL%s %s:%d: a CALLS edge unexpectedly targets %s " "(expected NONE — callee is unresolvable)\n", tf_red(), tf_reset(), __FILE__, __LINE__, callee_substr); rc = 1; } rh_cleanup(&lp, store); return rc; } /* * assert_strategy_absent — assert a given strategy tag NEVER surfaces on any * CALLS edge. Used for the bare "lsp_ts" probe: the default fallback tag is * never emitted as a distinct strategy (every concrete site passes a literal * "lsp_ts_*"), and the fixture is an UNRESOLVED call (no "lsp_ts_*" edge to * substring-alias against), so its absence is the accurate, intended invariant. * Returns 0 on PASS (tag absent), non-zero on FAIL (tag unexpectedly present). */ static int assert_strategy_absent(const char *filename, const char *src, const char *strategy) { RProj lp; cbm_store_t *store = rh_index(&lp, filename, src); if (!store) { printf(" %sFAIL%s %s:%d: index failed for absent-strategy %s\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy); rh_cleanup(&lp, store); return 1; } int rc = 0; if (inv_edge_has_strategy(store, lp.project, strategy)) { printf(" %sFAIL%s %s:%d: strategy %s unexpectedly PRESENT on a CALLS " "edge (expected ABSENT — bare fallback tag is never emitted)\n", tf_red(), tf_reset(), __FILE__, __LINE__, strategy); rc = 1; } rh_cleanup(&lp, store); return rc; } /* ── Fixtures ──────────────────────────────────────────────────────────────── * * Each fixture is the MINIMAL construct ts_lsp.c keys on for one strategy. The * call we care about always lives inside a function or method so callable- * sourcing is testable; the callee is also defined in-file (or in a sibling file * for the cross-file import strategies) so the registry can resolve it. * ───────────────────────────────────────────────────────────────────────── */ /* lsp_ts_local — bare identifier call f() that resolves to a module-local * function (ts_lsp.c:2310-2322: call_expression function is an `identifier`, * cbm_registry_lookup_symbol_by_args hits on the module QN). */ static const char kTsLocal[] = "function helper(x: number): number { return x + 1; }\n" "function caller(v: number): number { return helper(v); }\n"; /* lsp_ts_method — obj.method() type-based dispatch on a NAMED in-file class * receiver (ts_lsp.c:2257-2284: member_expression, ts_eval_expr_type gives the * receiver's NAMED type, lookup_method finds the method). */ static const char kTsMethod[] = "class Counter {\n" " inc(x: number): number { return x + 1; }\n" "}\n" "function caller(): number {\n" " const c = new Counter();\n" " return c.inc(1);\n" "}\n"; /* lsp_ts_namespace — Ns.fn() where Ns is a namespace import * (`import * as Ns from "./mod"`). ts_lsp.c:2233-2246: the member_expression * object is an `identifier` matching an import local name; fn resolves in that * imported module's registry → lsp_ts_namespace. Cross-file: util.ts exports the * function, main.ts imports the namespace and calls Util.compute(). */ static const RFile kTsNamespace[] = { {"util.ts", "export function compute(x: number): number { return x * 3; }\n"}, {"main.ts", "import * as Util from \"./util\";\n" "function caller(v: number): number { return Util.compute(v); }\n"}, }; /* lsp_ts_import — bare identifier call to an imported function * (`import { helper } from "./mod"`). ts_lsp.c:2327-2334: the call_expression * function is an `identifier` matching an import local name; helper resolves in * the imported module's registry → lsp_ts_import. Cross-file: util.ts exports * helper, main.ts imports it by name and calls it bare. */ static const RFile kTsImport[] = { {"util.ts", "export function helper(x: number): number { return x + 5; }\n"}, {"main.ts", "import { helper } from \"./util\";\n" "function caller(v: number): number { return helper(v); }\n"}, }; /* lsp_ts_jsx — JSX element whose tag is a module-local component * function (ts_lsp.c:2643-2647). TSX only (jsx_mode); the tag's first letter is * uppercase so it is NOT treated as an intrinsic HTML element; it resolves via * cbm_registry_lookup_symbol on the module QN. App() renders defined * in the same file. */ static const char kTsxJsx[] = "function Widget(): any { return null; }\n" "function App(): any {\n" " return ;\n" "}\n"; /* lsp_ts_jsx_import — JSX element whose tag is an imported component * (ts_lsp.c:2652-2657). TSX only; the tag matches an import local name → a * synthetic "." callee QN is emitted (this site does NOT verify * the symbol is in the registry). Cross-file: widget.tsx exports Widget, * app.tsx imports it and renders . */ static const RFile kTsxJsxImport[] = { {"widget.tsx", "export function Widget(): any { return null; }\n"}, {"app.tsx", "import { Widget } from \"./widget\";\n" "function App(): any {\n" " return ;\n" "}\n"}, }; /* lsp_ts — the DEFAULT fallback strategy inside ts_emit_resolved_call * (ts_lsp.c:116): used only when a caller passes a NULL strategy. Every concrete * emit site passes a literal "lsp_ts..." string, so "lsp_ts" is never emitted as * a distinct tag. This fixture is an ordinary resolved local call; we assert * whether the bare "lsp_ts" tag ever surfaces. EXPECTED ABSENT (RED): if it goes * GREEN, a new NULL-strategy emit site has appeared and should be audited. * NOTE: inv_edge_has_strategy does a substring match, and "lsp_ts" is a prefix of * "lsp_ts_local"/"lsp_ts_method"/etc., so a local-call fixture would substring- * match "lsp_ts" via "lsp_ts_local" and report a false GREEN. To probe the bare * tag in isolation we use an UNRESOLVED call (totallyUnknownFn) whose only * possible tag is the unresolved marker — there is no "lsp_ts_*" edge to alias * against, so a GREEN here would mean a literal bare "lsp_ts" edge exists. */ static const char kTsDefault[] = "function caller(v: number): number { return totallyUnknownFn(v); }\n"; /* lsp_unresolved — a call to a function not in the registry; the resolver * records the fallback marker via ts_emit_unresolved_call (ts_lsp.c:122-132, * strategy = "lsp_unresolved", confidence 0.0). A 0.0-confidence unresolved entry * is typically NOT promoted into a CALLS edge carrying the strategy tag, so this * is EXPECTED ABSENT (RED) — it documents whether "lsp_unresolved" surfaces in * the graph. */ static const char kTsUnresolved[] = "function known(x: number): number { return x + 1; }\n" "function caller(v: number): number { return known(v) + totallyUnknownFn(v); }\n"; /* ── Per-strategy tests ──────────────────────────────────────────────────── */ TEST(repro_lsp_ts_local) { return assert_lsp_strategy("main.ts", kTsLocal, "lsp_ts_local"); } TEST(repro_lsp_ts_method) { return assert_lsp_strategy("main.ts", kTsMethod, "lsp_ts_method"); } TEST(repro_lsp_ts_namespace) { return assert_lsp_strategy_files(kTsNamespace, (int)(sizeof(kTsNamespace) / sizeof(kTsNamespace[0])), "lsp_ts_namespace"); } TEST(repro_lsp_ts_import) { return assert_lsp_strategy_files( kTsImport, (int)(sizeof(kTsImport) / sizeof(kTsImport[0])), "lsp_ts_import"); } TEST(repro_lsp_ts_jsx) { return assert_lsp_strategy("app.tsx", kTsxJsx, "lsp_ts_jsx"); } TEST(repro_lsp_ts_jsx_import) { return assert_lsp_strategy_files(kTsxJsxImport, (int)(sizeof(kTsxJsxImport) / sizeof(kTsxJsxImport[0])), "lsp_ts_jsx_import"); } TEST(repro_lsp_ts_default) { /* The bare "lsp_ts" fallback tag is never emitted as a distinct strategy * (every concrete site passes a literal "lsp_ts_*"); the fixture is an * UNRESOLVED call with no "lsp_ts_*" edge to substring-alias against. Per the * fixture header, the accurate invariant is that "lsp_ts" is ABSENT. */ return assert_strategy_absent("main.ts", kTsDefault, "lsp_ts"); } TEST(repro_lsp_ts_unresolved) { /* totallyUnknownFn is UNDECLARED — no node can exist for it, so no CALLS * edge can ever form. Assert the accurate no-resolvable-edge behaviour * instead of a resolution strategy on an edge (unachievable by design). */ return assert_no_resolvable_edge("main.ts", kTsUnresolved, "totallyUnknownFn"); } /* ── Suite ───────────────────────────────────────────────────────────────── */ SUITE(repro_lsp_ts) { RUN_TEST(repro_lsp_ts_local); RUN_TEST(repro_lsp_ts_method); RUN_TEST(repro_lsp_ts_namespace); RUN_TEST(repro_lsp_ts_import); RUN_TEST(repro_lsp_ts_jsx); RUN_TEST(repro_lsp_ts_jsx_import); RUN_TEST(repro_lsp_ts_default); RUN_TEST(repro_lsp_ts_unresolved); }