diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 302fee5b7..a7f4286ef 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -1873,7 +1873,11 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk if (cbm_kind_in_set(node, spec->call_node_types)) { char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); - if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) { + // Keyword-filter callees, but keep builtins we mint a node for (len, str, + // ...) so the LSP-resolved builtin call still forms a CALLS edge. + if (callee && callee[0] && + (!cbm_is_keyword(callee, ctx->language) || + cbm_is_resolvable_builtin(callee, ctx->language))) { CBMCall call = {0}; call.callee_name = callee; call.enclosing_func_qn = state->enclosing_func_qn; diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 5821eefbc..3cea43ab3 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -210,6 +210,29 @@ bool cbm_is_keyword(const char *name, CBMLanguage lang) { return false; } +// Builtins that appear in the keyword set above (so they are suppressed as bare +// usages) but for which we mint a real graph node and an LSP resolution, so a +// CALL to them must still be extracted. MUST stay in sync with kPyBuiltinNodes +// in internal/cbm/lsp/py_builtins.c — every entry here has a "builtins." +// node, so the resulting CALLS edge always has a target (never Module-sourced). +static const char *python_resolvable_builtins[] = {"len", "print", "str", "int", + "list", "dict", "range", NULL}; + +bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang) { + if (!name || !name[0]) { + return false; + } + if (lang != CBM_LANG_PYTHON) { + return false; + } + for (const char **b = python_resolvable_builtins; *b; b++) { + if (strcmp(name, *b) == 0) { + return true; + } + } + return false; +} + // --- Export detection --- bool cbm_is_exported(const char *name, CBMLanguage lang) { diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 232db84d1..6fb136294 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -15,6 +15,14 @@ char *cbm_node_text(CBMArena *a, TSNode node, const char *source); // Check if a string is a language keyword (should be skipped as callee/usage). bool cbm_is_keyword(const char *name, CBMLanguage lang); +// Check if a name is a builtin we mint a real graph node for, so a CALL to it +// must NOT be keyword-filtered out of call extraction (the LSP resolves it to +// the injected builtin node and forms a CALLS edge). Narrower than +// cbm_is_keyword: it only covers builtins with a target node, so un-filtering +// them cannot produce a node-less / Module-sourced edge. The Python set MUST +// stay in sync with kPyBuiltinNodes in internal/cbm/lsp/py_builtins.c. +bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang); + // Classify a string literal as URL, config, or neither. // Returns CBM_STRREF_URL (0), CBM_STRREF_CONFIG (1), or -1 for neither. int cbm_classify_string(const char *str, int len); diff --git a/internal/cbm/lsp/kotlin_builtins.c b/internal/cbm/lsp/kotlin_builtins.c new file mode 100644 index 000000000..50d596a45 --- /dev/null +++ b/internal/cbm/lsp/kotlin_builtins.c @@ -0,0 +1,71 @@ +/* + * kotlin_builtins.c — Minimal Kotlin universal builtins as real graph nodes. + * + * When a method call lands on an unknown-typed receiver and the member is one + * of the universal kotlin.Any methods (toString / equals / hashCode), the + * Kotlin LSP resolves it to "kotlin.Any." and emits the lsp_kt_any + * strategy (kotlin_lsp.c, kt_emit_resolved). Any is the supertype of every + * Kotlin reference, so this is the same target the fwcd LSP resolves to. + * + * The missing piece is downstream: pass_calls.c only writes a CALLS edge when + * cbm_pipeline_lsp_target_node() resolves the callee_qn to a graph node + * (src/pipeline/lsp_resolve.h). There is no "kotlin.Any" node in the graph, so + * the resolved call is dropped and the strategy never lands on an edge + * (callable=0). + * + * Fix: inject a small, fixed set of kotlin.Any definitions into result->defs + * during the per-file Kotlin LSP run (cbm_run_kotlin_lsp, which executes inside + * cbm_extract_file, BEFORE the parallel pipeline mints def nodes from + * result->defs). The graph therefore gains real "kotlin.Any[.]" nodes + * that the lsp_kt_any edges target. The QNs here MUST match what kt_emit_resolved + * emits as callee_qn ("kotlin.Any."). + * + * Node minting upserts by QN (cbm_gbuf_upsert_node), so injecting the same + * builtins per Kotlin file collapses to one node per QN — no duplicates. + * + * Self-contained: #included from kotlin_lsp.c only (amalgamation pattern; see + * lsp_all.c). Not a standalone translation unit. Mirror of py_builtins.c. + */ + +/* A single builtin entry to mint as a graph node. */ +typedef struct { + const char *qn; /* graph QN — MUST equal the kt_emit_resolved callee_qn */ + const char *name; /* short name (last segment of qn) */ + const char *label; /* "Class" | "Method" */ +} KtBuiltinNode; + +/* + * Universal kotlin.Any members the LSP falls back to (kt_any_methods in + * kotlin_lsp.c). The Any class node anchors the three methods. + */ +static const KtBuiltinNode kKtBuiltinNodes[] = { + {"kotlin.Any", "Any", "Class"}, + {"kotlin.Any.toString", "toString", "Method"}, + {"kotlin.Any.equals", "equals", "Method"}, + {"kotlin.Any.hashCode", "hashCode", "Method"}, +}; + +/* + * Inject the builtin definitions into result->defs so the pipeline mints them + * as graph nodes. All fields beyond name/qn/label are left zero/NULL: builtins + * have no body, so complexity/line-range/etc. are irrelevant, and a synthetic + * file_path keeps them out of any real source file's def list. + */ +static void kt_builtins_inject_defs(CBMFileResult *result, CBMArena *arena) { + if (!result || !arena) { + return; + } + const int n = (int)(sizeof(kKtBuiltinNodes) / sizeof(kKtBuiltinNodes[0])); + for (int i = 0; i < n; i++) { + const KtBuiltinNode *b = &kKtBuiltinNodes[i]; + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = b->name; + def.qualified_name = b->qn; + def.label = b->label; + def.file_path = ""; + def.start_line = 1; + def.end_line = 1; + cbm_defs_push(&result->defs, arena, def); + } +} diff --git a/internal/cbm/lsp/kotlin_lsp.c b/internal/cbm/lsp/kotlin_lsp.c index 84fc07cdb..7a3d843a6 100644 --- a/internal/cbm/lsp/kotlin_lsp.c +++ b/internal/cbm/lsp/kotlin_lsp.c @@ -42,6 +42,10 @@ #include #include +/* Minimal Kotlin universal builtins as real graph nodes (kt_builtins_inject_defs). + * Amalgamation-included (see lsp_all.c); mirror of py_builtins.c. */ +#include "kotlin_builtins.c" + #define KT_EVAL_MAX_DEPTH 32 #define KT_IMPORT_INITIAL_CAP 16 @@ -4117,6 +4121,10 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour kotlin_lsp_process_file(&ctx, use_root); + /* Inject kotlin.Any universal-method nodes (toString/equals/hashCode) so the + * lsp_kt_any fallback emitted above has a target node to form a CALLS edge. */ + kt_builtins_inject_defs(result, arena); + if (patched_tree) { ts_tree_delete(patched_tree); } diff --git a/tests/repro/repro_issue431.c b/tests/repro/repro_issue431.c index 4fddecb35..17e1dc82b 100644 --- a/tests/repro/repro_issue431.c +++ b/tests/repro/repro_issue431.c @@ -109,14 +109,17 @@ TEST(repro_issue431_vscode_profile_inherits_mcp_json) { /* --- Precondition: VSCode is detected --- */ cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); if (!agents.vscode) { - /* Detection failed in the temp tree — adjust path derivation. - * On non-Apple Linux the detection reads cbm_app_config_dir() which - * is process-global (not home-relative), so detection may return false - * for a synthetic tmpdir home. The bug still exists, but we cannot - * demonstrate it via the plan-based oracle without detection firing. - * Mark the test as an expected skip on this platform/config. */ + /* #431 IS FIXED: install_vscode_profile_configs() (cli.c:3211) scans + * Code/User/profiles/ and plans a per-profile mcp.json, so the assertion + * below passes as a genuine regression guard whenever detection fires + * (which it does for this fixture). This branch is only reached if + * cbm_detect_agents() cannot see the fixture home on some platform — in + * which case the fix cannot be VERIFIED here. Skip honestly rather than + * vacuously PASS (would hide a future regression) or FAIL (would red a + * fixed bug). */ th_rmtree(tmpdir); - PASS(); /* precondition unmet — non-blocking; bug still open */ + SKIP_PLATFORM("VSCode detection did not fire for the synthetic fixture " + "home; cannot verify the #431 per-profile install here"); } /* --- Run the install plan oracle (dry-run, no mutations) --- */ diff --git a/tests/repro/repro_issue581.c b/tests/repro/repro_issue581.c index a7ae514f6..a2151da25 100644 --- a/tests/repro/repro_issue581.c +++ b/tests/repro/repro_issue581.c @@ -245,46 +245,41 @@ TEST(repro_issue581_query_rss_stable) { free(args); rh_cleanup(&lp, store); - // If RSS is not measurable (cbm_mem_rss() returns 0 and no Linux fallback), - // skip the growth assertion -- an unmeasurable RSS cannot produce a - // meaningful signal. This avoids a false PASS masking a real leak on - // platforms where our RSS API is unavailable. - if (rss_warmup == 0 || rss_end == 0) { - printf(" NOTE: RSS not measurable on this platform/build; " - "growth assertion skipped (inconclusive, not a pass)\n"); - PASS(); + if (rss_warmup > 0 && rss_end > 0) { + printf(" rss_warmup_kb=%zu rss_end_kb=%zu factor=%.2f threshold=%.1f\n", rss_warmup / 1024, + rss_end / 1024, (double)rss_end / (double)rss_warmup, LEAK_FACTOR); + } else { + printf(" NOTE: RSS not measurable on this platform/build\n"); } - printf(" rss_warmup_kb=%zu rss_end_kb=%zu factor=%.2f threshold=%.1f\n", - rss_warmup / 1024, rss_end / 1024, - (double)rss_end / (double)rss_warmup, - LEAK_FACTOR); - - // PRIMARY assertion: end RSS must not exceed LEAK_FACTOR x warmup RSS. + // HONEST RED — this guard is currently VACUOUS and #581 is OPEN. // - // RED condition (current code): - // SQLite WAL + mimalloc retained pages grow each iteration. - // Over 150 iterations the cumulative growth pushes rss_end above - // LEAK_FACTOR * rss_warmup. - // ASSERT fires -> RED. + // This fixture CANNOT reproduce the leak: a 3-node graph over 150 + // search_graph calls allocates far too little to move process RSS (observed + // factor=1.00), so the old "rss_end <= 3.0 x rss_warmup" assertion passed + // even on the leaking build. A green here would mean "leak fixed" while the + // leak is unfixed — a false guard that violates the tests-are-guards rule + // (green <=> fixed). So it stays RED. // - // GREEN condition (after fix): - // Periodic compaction (cbm_mem_collect + WAL TRUNCATE checkpoint) keeps - // rss_end near rss_warmup. factor stays <1.5 comfortably. + // Turning this GREEN legitimately requires BOTH: + // (a) a real reproduction tier — a long-running MCP session issuing + // thousands of ops against a LARGE graph, measuring the SQLite WAL + // file size and mimalloc committed pages DIRECTLY (not process-RSS + // jitter) so the monotonic growth is actually observable; AND + // (b) the fix — periodic SQLITE_CHECKPOINT_TRUNCATE + cbm_mem_collect() in + // the MCP query loop / idle eviction (see the header + #581). // - // We report the ratio in the failure message so the fixer can see the - // growth slope without needing a profiler. - size_t rss_limit = (size_t)(rss_warmup * LEAK_FACTOR); - if (rss_end > rss_limit) { - printf(" BUG #581 reproduced: RSS grew %.2fx after %d search_graph calls " - "(warmup=%zu kB end=%zu kB limit=%zu kB)\n", - (double)rss_end / (double)rss_warmup, - ITER_TOTAL - ITER_WARMUP, - rss_warmup / 1024, rss_end / 1024, rss_limit / 1024); - } - ASSERT(rss_end <= rss_limit); - - PASS(); + // Until both land this is an honest "not fixed / not provable here" RED, not + // a false green. + /* TODO(#581): whitelisted known-red on the non-gating bug-repro board. The + * leak is a real OPEN bug; this fixture cannot yet reproduce it, so the test + * stays RED (honest "not fixed") rather than vacuously green. Turning it + * green requires a real WAL-size / mimalloc-committed-pages reproduction tier + * plus the query-path compaction fix (see header). Tracked, not skipped. */ + FAIL("TODO(#581) whitelisted known-red: query-path memory leak is OPEN and " + "cannot be reproduced in this fixture (RSS factor ~1.0 even when " + "leaking) — needs a real WAL/committed-pages reproduction tier plus the " + "query-path compaction fix"); } // -- Suite ------------------------------------------------------------------ diff --git a/tests/repro/repro_lsp_c_cpp.c b/tests/repro/repro_lsp_c_cpp.c index a94f2e25a..4e89f755e 100644 --- a/tests/repro/repro_lsp_c_cpp.c +++ b/tests/repro/repro_lsp_c_cpp.c @@ -156,6 +156,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, 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", @@ -302,12 +317,12 @@ static const char kFuncPtr[] = * (RED) — it documents that the DLL-resolution path needs an external binding * the single-file harness can't synthesize. The fixture below at least exercises * a pointer assigned from an extern declaration. */ -static const char kDllResolve[] = - "extern int plugin_entry(int x);\n" - "int caller(int v) {\n" - " int (*fp)(int) = plugin_entry;\n" - " return fp(v);\n" - "}\n"; +static const char kDllResolve[] = "extern int plugin_entry(int x);\n" + "int known(int x) { return x + 1; }\n" + "int caller(int v) {\n" + " int (*fp)(int) = plugin_entry;\n" + " return known(v) + fp(v);\n" + "}\n"; /* lsp_operator — overloaded binary operator+ on a custom type (c_lsp.c:3771-3789: * binary_expression, lhs is a custom type, operator+ member found). */ @@ -387,10 +402,10 @@ static const char kAdl[] = * called with a NULL callee_qn; the more common unresolved path is * c_emit_unresolved_call (a different marker). This fixture exercises a call to * an undeclared function and documents whether "lsp_unresolved" surfaces. */ -static const char kUnresolved[] = - "int caller(int v) {\n" - " return totally_unknown_fn(v);\n" - "}\n"; +static const char kUnresolved[] = "int known(int x) { return x + 1; }\n" + "int caller(int v) {\n" + " return known(v) + totally_unknown_fn(v);\n" + "}\n"; /* ── Per-strategy tests ──────────────────────────────────────────────────── */ diff --git a/tests/repro/repro_lsp_go_py.c b/tests/repro/repro_lsp_go_py.c index d83077ca6..fcb06903d 100644 --- a/tests/repro/repro_lsp_go_py.c +++ b/tests/repro/repro_lsp_go_py.c @@ -201,6 +201,21 @@ static int assert_no_resolvable_edge_files(const RFile *files, int nfiles, 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", @@ -309,11 +324,11 @@ static const RFile kGoCrossFile[] = { * "lsp_unresolved"). NOTE: emit_unresolved_call uses confidence 0.0, so the * pipeline may not promote it into a CALLS edge with the strategy tag — this * fixture documents whether "lsp_unresolved" surfaces in the graph. */ -static const char kGoUnresolved[] = - "package main\n" - "func caller(v int) int {\n" - " return totallyUnknownFn(v)\n" - "}\n"; +static const char kGoUnresolved[] = "package main\n" + "func known(x int) int { return x + 1 }\n" + "func caller(v int) int {\n" + " return known(v) + totallyUnknownFn(v)\n" + "}\n"; /* ── Python fixtures ───────────────────────────────────────────────────────── */ @@ -386,13 +401,13 @@ static const RFile kPyModuleAttr[] = { * symbol lookup misses → best-effort "module.attr" QN, low confidence). helpers * defines nothing named missing_fn. */ static const RFile kPyModuleAttrUnresolved[] = { - {"helpers.py", - "def do_work(x):\n" - " return x + 9\n"}, - {"main.py", - "import helpers\n" - "def caller(v):\n" - " return helpers.missing_fn(v)\n"}, + {"helpers.py", "def do_work(x):\n" + " return x + 9\n"}, + {"main.py", "import helpers\n" + "def known(x):\n" + " return x + 1\n" + "def caller(v):\n" + " return known(v) + helpers.missing_fn(v)\n"}, }; /* lsp_dict_dispatch — funcs["key"]() where funcs is a dict-literal dispatch @@ -571,23 +586,14 @@ TEST(repro_lsp_py_operator_dunder) { } TEST(repro_lsp_py_builtin) { - /* PARKED for release: lsp_builtin (len(v)) needs a typeshed/builtins registry - * so builtin functions have target nodes; without it the resolution has no - * node to form a CALLS edge to (callable=0). Tracked for a future builtins - * registry. */ - printf(" %sSKIP%s parked: needs builtins/typeshed registry (len has no node)\n", tf_dim(), - tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* len(v) resolves to the injected builtins.len node (py_builtins.c) and + * emits lsp_builtin with a real CALLS edge. */ return assert_lsp_strategy("main.py", kPyBuiltin, "lsp_builtin"); } TEST(repro_lsp_py_builtin_constructor) { - /* PARKED for release: lsp_builtin_constructor (str(v)) needs a builtins/ - * typeshed registry so the builtin type str has a node to target. Tracked - * for a future builtins registry. */ - printf(" %sSKIP%s parked: needs builtins/typeshed registry (str type has no node)\n", tf_dim(), - tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* str(v) resolves to the injected builtins.str type node (py_builtins.c) + * and emits lsp_builtin_constructor with a real CALLS edge. */ return assert_lsp_strategy("main.py", kPyBuiltinConstructor, "lsp_builtin_constructor"); } diff --git a/tests/repro/repro_lsp_java_cs.c b/tests/repro/repro_lsp_java_cs.c index a898f8795..fed8a6121 100644 --- a/tests/repro/repro_lsp_java_cs.c +++ b/tests/repro/repro_lsp_java_cs.c @@ -174,6 +174,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, 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", @@ -245,7 +260,8 @@ static const char kJavaStaticImport[] = static const char kJavaStaticImportText[] = "import static java.lang.Math.max;\n" "class Client {\n" - " int run(int a, int b) { return max(a, b); }\n" + " int known(int x) { return x + 1; }\n" + " int run(int a, int b) { return known(a) + max(a, b); }\n" "}\n"; /* lsp_super_dispatch — super.method() resolves on the superclass @@ -375,7 +391,8 @@ static const char kJavaConstructorSynth[] = * surfaces on a CALLS edge at all. */ static const char kJavaUnresolved[] = "class Client {\n" - " int run(int v) { return totallyUnknownFn(v); }\n" + " int known(int x) { return x + 1; }\n" + " int run(int v) { return known(v) + totallyUnknownFn(v); }\n" "}\n"; /* ── C# fixtures ───────────────────────────────────────────────────────────── @@ -402,7 +419,7 @@ static const char kCsStaticTypedUnindexed[] = " public static int Known() { return 1; }\n" "}\n" "class Client {\n" - " public int Run() { return Helper.Missing(); }\n" + " public int Run() { return Helper.Known() + Helper.Missing(); }\n" "}\n"; /* cs_method_typed — obj.Method() on the object's OWN declared type @@ -450,7 +467,7 @@ static const char kCsMethodTypedUnindexed[] = " public int Inc(int x) { return x + 1; }\n" "}\n" "class Client {\n" - " public int Run(Counter c) { return c.Missing(); }\n" + " public int Run(Counter c) { return c.Inc(1) + c.Missing(); }\n" "}\n"; /* cs_self_method — a bare Method() resolved on the enclosing class diff --git a/tests/repro/repro_lsp_kt_php_rust.c b/tests/repro/repro_lsp_kt_php_rust.c index e5a801773..d51d517c2 100644 --- a/tests/repro/repro_lsp_kt_php_rust.c +++ b/tests/repro/repro_lsp_kt_php_rust.c @@ -536,13 +536,9 @@ TEST(repro_lsp_kt_lambda_it) { return assert_lsp_strategy("main.kt", kKtLambdaIt, "lsp_kt_lambda_it"); } TEST(repro_lsp_kt_any) { - /* PARKED for release: `x.toString()` on an unknown-typed receiver resolves to - * kotlin.Any.toString — a builtin with no node in the project, so no CALLS - * edge can form (callable=0). Needs an Any/builtin node (a kotlin stdlib - * registry) to anchor the edge. */ - printf(" %sSKIP%s parked: needs a kotlin.Any/builtin node (toString has no target)\n", - tf_dim(), tf_reset()); - return -1; /* skip — not counted as pass or fail */ + /* x.toString() on an unknown-typed receiver resolves to kotlin.Any.toString + * (the universal-method fallback) and forms a CALLS edge to the injected + * kotlin.Any.toString node (kotlin_builtins.c). */ return assert_lsp_strategy("main.kt", kKtAny, "lsp_kt_any"); } TEST(repro_lsp_kt_destructure) { diff --git a/tests/repro/repro_lsp_ts.c b/tests/repro/repro_lsp_ts.c index 38dee95c1..509679eb6 100644 --- a/tests/repro/repro_lsp_ts.c +++ b/tests/repro/repro_lsp_ts.c @@ -193,6 +193,21 @@ static int assert_no_resolvable_edge(const char *filename, const char *src, 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", @@ -333,7 +348,8 @@ static const char kTsDefault[] = * is EXPECTED ABSENT (RED) — it documents whether "lsp_unresolved" surfaces in * the graph. */ static const char kTsUnresolved[] = - "function caller(v: number): number { return totallyUnknownFn(v); }\n"; + "function known(x: number): number { return x + 1; }\n" + "function caller(v: number): number { return known(v) + totallyUnknownFn(v); }\n"; /* ── Per-strategy tests ──────────────────────────────────────────────────── */ diff --git a/tests/test_grammar_labels.c b/tests/test_grammar_labels.c index 5f3bd324c..21f1a4704 100644 --- a/tests/test_grammar_labels.c +++ b/tests/test_grammar_labels.c @@ -86,7 +86,9 @@ static const LabelGolden LABEL_GOLDENS[] = { {"typescript", "Class:1,Function:1,Module:1"}, {"tsx", "Function:1,Module:1"}, {"java", "Class:1,Method:1,Module:1"}, - {"kotlin", "Class:1,Function:1,Module:1"}, + /* +Class:1 (kotlin.Any) +Method:3 (toString/equals/hashCode) injected by + * kotlin_builtins.c, as python's golden includes its injected builtins. */ + {"kotlin", "Class:2,Function:1,Method:3,Module:1"}, {"rust", "Function:1,Module:1,Struct:1"}, {"ruby", "Class:1,Function:1,Module:1"}, {"php", "Class:1,Function:1,Module:1"},