mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
`CBMDefinition.base_classes` carries the SOURCE SPELLING of each base
("Base", "django.db.Model"): 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 found no member and
fell through to the weak textual cascade, where the receiver-aware guard
(#592/#606) correctly kills it. The member-lookup walk over
embedded_types was already there in both languages; only the names it
walked were unusable across files.
cbm_pxc_collect_all_defs now resolves each base spelling to a project QN
using 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 rather than a short-name guess, so the CALLS edge
it enables is a supported fact that the weak-member guard keeps.
Weak registry strategies (suffix_match / unique_name / field_type_hint /
fuzzy) are rejected for bases via an explicit drop-list: a base bound
because some project type happens to share its name is precisely the
fabricated relation #606 removed, and inheritance multiplies it — every
inherited member of the wrong base would become a callable target. An
unresolved base keeps its raw spelling and the behaviour that predates
this change, so stdlib and third-party bases are unaffected.
Scoped to the languages whose registrars read embedded_types as QNs
(Python, JS/TS/TSX). Go, JVM, C#, C++ and Rust already qualify their own
embedded types and keep the raw join.
Cost is O(defs) hash lookups plus ONE import map per file: no per-call-
site hierarchy walk, no registry scan, no per-file registry rebuild. The
complexity guard stays linear (nodes 1.93, edges 2.07, per-file defs
2.00 against a files x corpus coupling of ~4).
Tests:
- ts/S6 flips to its own documented condition: was `calls == 0` as a
tripwire recording the gap, now asserts calls >= 1 AND an lsp_ts_*
strategy. Measured: strategy=lsp_ts_method, confidence 0.95.
- python/S6b is new and covers what python/S6 structurally cannot: S6's
`def run(c)` parameter is un-annotated, so no receiver type exists to
inherit through and any binding there is a guess about a name (it
resolves via unique_name at 0.75). S6b supplies the receiver three ways
— annotated parameter, `self` in the subclass, constructor result — and
asserts INHERITS >= 1 plus an lsp_* strategy. Revert-check: all three
fall back to unique_name at 0.75 without this change, lsp_method at
0.90 with it.
- Both rows assert the STRATEGY, not just the edge count: a short-name
guess and a resolved inherited member produce the same count in a
two-file fixture, and only the strategy tells them apart.
- Corrects python/S6's stale comment citing a base_classes extraction
bug; extraction_inheritance is green for Python and TS, and
base_classes now holds clean names.
macOS full suite 7690 passed / 0 failed / 8 skipped (140 suites);
make lint-ci clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1971 lines
101 KiB
C
1971 lines
101 KiB
C
/*
|
||
* test_lsp_resolution_probe.c — Aggressive per-scenario LSP type-resolution probe.
|
||
*
|
||
* REPRODUCE-FIRST — green cases are permanent regression guards; red cases are
|
||
* reproduction tests for bugs the call-graph-count check missed. Do NOT
|
||
* register this suite in test_main.c (a separate agent owns that file).
|
||
*
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
* WHAT THIS FILE PROBES
|
||
* ──────────────────────
|
||
* The hybrid LSP TYPE-RESOLUTION PASS (pass_lsp_cross.c / cbm_run_X_lsp_cross)
|
||
* is wired for: Go, C, C++, CUDA, Python, JavaScript, TypeScript, TSX, PHP.
|
||
* It is NOT wired (cbm_pxc_has_cross_lsp returns false) for:
|
||
* - Rust — cbm_run_rust_lsp_cross EXISTS in rust_lsp.c but is never called
|
||
* - C# — cbm_run_cs_lsp_cross EXISTS in cs_lsp.c but is never called
|
||
* - Java — cbm_run_java_lsp_cross EXISTS in java_lsp.h but is never called
|
||
* - Kotlin — NO cross-file LSP function; uses per-file cbm_run_kotlin_lsp only
|
||
*
|
||
* All four missing-wiring cases fall through to the generic name-based resolver
|
||
* (pass_calls.c). That resolver IS project-wide, so plain same-package function
|
||
* calls still resolve. What it CANNOT resolve are type-dispatch scenarios where
|
||
* receiver-type knowledge is required (method dispatch, constructor, static call,
|
||
* chained call, inherited method, generics, field-type-hint).
|
||
*
|
||
* The 8 probe scenarios (applied to each of the 9 hybrid-LSP languages where
|
||
* applicable):
|
||
* S1 — cross-file plain function call (already covered by test_edge_structural.c
|
||
* for CALLS presence; here we add more flavours and double as sanity guards)
|
||
* S2 — method dispatch via receiver type (obj.method() with known type)
|
||
* S3 — constructor / instantiation (new T() / T{} / T() → CALLS/USAGE to type)
|
||
* S4 — static / class-method call (T.staticM() / T::staticM())
|
||
* S5 — chained call (a.b().c() — second call resolved via first
|
||
* return type)
|
||
* S6 — inherited-method call (subclass instance calls base-class method)
|
||
* S7 — generic/templated call (call through parameterized type)
|
||
* S8 — field-type-hint (this.field.method() where field has declared type)
|
||
*
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
* EXPECTED GREEN / RED REASONING
|
||
* ──────────────────────────────
|
||
* Languages with lsp_cross WIRED (Go, C, C++, Python, TypeScript, PHP):
|
||
* S1 (cross-file call): GREEN — generic resolver handles it; lsp_cross adds confidence.
|
||
* S2 (method dispatch): GREEN (Go/Python/TS/PHP have full type inference).
|
||
* S3 (constructor): GREEN for most; PHP new T() GREEN, C malloc pattern different.
|
||
* S4 (static call): GREEN for Go (pkg.Func), TS (Class.static), Python N/A.
|
||
* S5 (chained): GREEN for Go/TS/Python where return-type inference works.
|
||
* S6 (inherited): GREEN when inheritance chain is in same index.
|
||
* S7 (generic): GREEN for Go generics (1.18+) / TS generics with known type arg.
|
||
* S8 (field type hint): GREEN for Go/TS/Python where field types are declared.
|
||
*
|
||
* Languages WITHOUT lsp_cross (Rust, Java, Kotlin, C#):
|
||
* S1 (plain cross-file): GREEN — the generic name-based resolver handles it.
|
||
* S2 (method dispatch): RED — receiver type unknown without lsp_cross; resolver
|
||
* falls through to name-based (may still find it if
|
||
* the method name is unique; in ambiguous cases: RED).
|
||
* S3 (constructor): RED — type-instantiation CALLS from new T() needs type
|
||
* awareness; name-based resolver misses it.
|
||
* S4 (static call): RED — qualified T.Method() — name resolver finds "Method"
|
||
* but can't verify it belongs to T without type info.
|
||
* S5 (chained): RED — intermediate return type unknown → second call lost.
|
||
* S6 (inherited): RED — base-method call from subclass needs lsp_cross to
|
||
* resolve upward through the inheritance chain.
|
||
* S7 (generic): RED — parameterized type erased without lsp_cross.
|
||
* S8 (field type hint): RED — field-declared type unknown → this.field.method() lost.
|
||
*
|
||
* CAVEAT: "RED" means the specific TYPE-RESOLVED CALLS edge that lsp_cross would
|
||
* produce. For scenarios where the callee name is unambiguous in the project and the
|
||
* generic resolver finds it, the CALLS count may still be >=1 — we assert >= 1 for
|
||
* GREEN and diagnose (but don't require 0) for RED scenarios that may be partially
|
||
* covered by the name-based resolver. Each RED test asserts the CORRECT outcome
|
||
* (calls >= 1) and will fail / stay RED until lsp_cross is wired for that language.
|
||
*
|
||
* ════════════════════════════════════════════════════════════════════════════
|
||
* HARNESS
|
||
* ────────
|
||
* Mirrors test_edge_structural.c — self-contained static helpers with a LRP_
|
||
* prefix to avoid link conflicts.
|
||
*
|
||
* SUITE(lsp_resolution_probe) registers all RUN_TEST groups but is NOT wired
|
||
* into test_main.c. The harness infrastructure (tf_pass_count etc.) is
|
||
* declared extern — they live in test_main.c when linked into the full runner.
|
||
*/
|
||
|
||
#include "../src/foundation/compat.h"
|
||
#include "test_framework.h"
|
||
#include "test_helpers.h"
|
||
#include "cbm.h"
|
||
#include <mcp/mcp.h>
|
||
#include <store/store.h>
|
||
#include <pipeline/pipeline.h>
|
||
#include <foundation/log.h>
|
||
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
#if !defined(_WIN32)
|
||
#include <sys/wait.h>
|
||
#endif
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* Harness helpers (LRP_ prefix — avoids collision with other suites).
|
||
* ══════════════════════════════════════════════════════════════════════ */
|
||
|
||
typedef struct {
|
||
char tmpdir[256];
|
||
char dbpath[512];
|
||
char *project;
|
||
cbm_mcp_server_t *srv;
|
||
} LRP_Proj;
|
||
|
||
typedef struct {
|
||
const char *name; /* relative filename, may include '/' for subdirs */
|
||
const char *content;
|
||
} LRP_File;
|
||
|
||
static void lrp_to_fwd_slashes(char *p) {
|
||
for (; *p; p++) {
|
||
if (*p == '\\')
|
||
*p = '/';
|
||
}
|
||
}
|
||
|
||
static cbm_store_t *lrp_open_indexed(LRP_Proj *lp) {
|
||
/* Freed before reassigning: a fixture that indexes more than once would
|
||
* otherwise drop the previous heap name on the floor. Teardown frees the
|
||
* last one. */
|
||
free(lp->project);
|
||
lp->project = cbm_project_name_from_path(lp->tmpdir);
|
||
if (!lp->project)
|
||
return NULL;
|
||
const char *home = getenv("HOME");
|
||
if (!home)
|
||
home = "/tmp";
|
||
char cache_dir[512];
|
||
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
|
||
cbm_mkdir(cache_dir);
|
||
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
|
||
unlink(lp->dbpath);
|
||
lp->srv = cbm_mcp_server_new(NULL);
|
||
if (!lp->srv)
|
||
return NULL;
|
||
char args[700];
|
||
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
|
||
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
|
||
if (resp)
|
||
free(resp);
|
||
return cbm_store_open_path(lp->dbpath);
|
||
}
|
||
|
||
static cbm_store_t *lrp_index(LRP_Proj *lp, const LRP_File *files, int nfiles) {
|
||
memset(lp, 0, sizeof(*lp));
|
||
snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_lrp_XXXXXX");
|
||
if (!cbm_mkdtemp(lp->tmpdir))
|
||
return NULL;
|
||
lrp_to_fwd_slashes(lp->tmpdir);
|
||
for (int i = 0; i < nfiles; i++) {
|
||
char path[700];
|
||
snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name);
|
||
char *slash = strrchr(path, '/');
|
||
if (slash && slash > path + strlen(lp->tmpdir)) {
|
||
*slash = '\0';
|
||
cbm_mkdir_p(path, 0755);
|
||
*slash = '/';
|
||
}
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f)
|
||
return NULL;
|
||
fputs(files[i].content, f);
|
||
fclose(f);
|
||
}
|
||
return lrp_open_indexed(lp);
|
||
}
|
||
|
||
static void lrp_cleanup(LRP_Proj *lp, cbm_store_t *store) {
|
||
if (store)
|
||
cbm_store_close(store);
|
||
if (lp->srv) {
|
||
cbm_mcp_server_free(lp->srv);
|
||
lp->srv = NULL;
|
||
}
|
||
free(lp->project);
|
||
lp->project = NULL;
|
||
th_rmtree(lp->tmpdir);
|
||
unlink(lp->dbpath);
|
||
char wal[600], shm[600];
|
||
snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath);
|
||
snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath);
|
||
unlink(wal);
|
||
unlink(shm);
|
||
}
|
||
|
||
/* Returns USAGE edge count (-1 on DB failure). */
|
||
static int lrp_usage(LRP_Proj *lp, const LRP_File *files, int nfiles) {
|
||
cbm_store_t *store = lrp_index(lp, files, nfiles);
|
||
int n = store ? cbm_store_count_edges_by_type(store, lp->project, "USAGE") : -1;
|
||
lrp_cleanup(lp, store);
|
||
return n;
|
||
}
|
||
|
||
/* Diagnostic: dump edge histogram to stderr on failure. */
|
||
static const char *LRP_ALL_EDGE_TYPES[] = {
|
||
"CALLS", "CALL_REFERENCE", "DEFINES", "DEFINES_METHOD", "IMPORTS", "INHERITS",
|
||
"IMPLEMENTS", "USAGE", "DECORATES", "HANDLES", "HTTP_CALLS", "ASYNC_CALLS",
|
||
"OVERRIDE", "TESTS", "TESTS_FILE", "DATA_FLOWS", NULL};
|
||
|
||
static void lrp_diag(cbm_store_t *store, const char *project, const char *scenario) {
|
||
if (!store) {
|
||
fprintf(stderr, " [LRP] %s: no graph DB\n", scenario);
|
||
return;
|
||
}
|
||
char line[512] = {0};
|
||
for (int i = 0; LRP_ALL_EDGE_TYPES[i]; i++) {
|
||
int c = cbm_store_count_edges_by_type(store, project, LRP_ALL_EDGE_TYPES[i]);
|
||
if (c > 0 && strlen(line) < sizeof(line) - 40) {
|
||
char one[48];
|
||
snprintf(one, sizeof(one), "%s=%d ", LRP_ALL_EDGE_TYPES[i], c);
|
||
strncat(line, one, sizeof(line) - strlen(line) - 1);
|
||
}
|
||
}
|
||
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
|
||
* until the bug is fixed — same pattern as test_edge_structural.c. */
|
||
static int lrp_assert_calls(const LRP_File *files, int nfiles, int min_calls, const char *scenario,
|
||
int expect_green) {
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, files, nfiles);
|
||
int got = store ? cbm_store_count_edges_by_type(store, lp.project, "CALLS") : -1;
|
||
if (got < min_calls) {
|
||
fprintf(stderr, " [LRP] %s FAIL calls=%d expected>=%d %s\n", scenario, got, min_calls,
|
||
expect_green ? "(GREEN regression)" : "(RED reproduction)");
|
||
lrp_diag(store, lp.project, scenario);
|
||
} else if (!expect_green) {
|
||
/* Unexpectedly passing — the lsp_cross wiring may have been added. */
|
||
fprintf(stderr,
|
||
" [LRP] %s UNEXPECTED PASS calls=%d "
|
||
"(lsp_cross may now be wired — promote to GREEN)\n",
|
||
scenario, got);
|
||
}
|
||
lrp_cleanup(&lp, store);
|
||
return got >= min_calls;
|
||
}
|
||
|
||
/* Count one exact CALLS relationship by unique source/target node names.
|
||
* Returning -1 distinguishes a malformed fixture or DB failure from a genuine
|
||
* missing edge. This prevents broad CALLS totals (including macro/stdlib edges)
|
||
* from masking a missing source-specific relationship. */
|
||
static int lrp_exact_edge_by_name(cbm_store_t *store, const char *project, const char *edge_type,
|
||
const char *source_name, const char *target_name) {
|
||
if (!store || !project || !edge_type || !source_name || !target_name) {
|
||
return -1;
|
||
}
|
||
|
||
cbm_node_t *sources = NULL;
|
||
cbm_node_t *targets = NULL;
|
||
int source_count = 0;
|
||
int target_count = 0;
|
||
if (cbm_store_find_nodes_by_name(store, project, source_name, &sources, &source_count) !=
|
||
CBM_STORE_OK ||
|
||
cbm_store_find_nodes_by_name(store, project, target_name, &targets, &target_count) !=
|
||
CBM_STORE_OK ||
|
||
source_count != 1 || target_count != 1) {
|
||
cbm_store_free_nodes(sources, source_count);
|
||
cbm_store_free_nodes(targets, target_count);
|
||
return -1;
|
||
}
|
||
|
||
int64_t source_id = sources[0].id;
|
||
int64_t target_id = targets[0].id;
|
||
cbm_store_free_nodes(sources, source_count);
|
||
cbm_store_free_nodes(targets, target_count);
|
||
|
||
cbm_edge_t *edges = NULL;
|
||
int edge_count = 0;
|
||
if (cbm_store_find_edges_by_source_type(store, source_id, edge_type, &edges, &edge_count) !=
|
||
CBM_STORE_OK) {
|
||
cbm_store_free_edges(edges, edge_count);
|
||
return -1;
|
||
}
|
||
|
||
int matches = 0;
|
||
for (int i = 0; i < edge_count; i++) {
|
||
if (edges[i].target_id == target_id) {
|
||
matches++;
|
||
}
|
||
}
|
||
cbm_store_free_edges(edges, edge_count);
|
||
return matches;
|
||
}
|
||
|
||
static int lrp_exact_calls_by_name(cbm_store_t *store, const char *project, const char *source_name,
|
||
const char *target_name) {
|
||
return lrp_exact_edge_by_name(store, project, "CALLS", source_name, target_name);
|
||
}
|
||
|
||
static int lrp_exact_edge_by_qn_suffix(cbm_store_t *store, const char *project,
|
||
const char *edge_type, const char *source_suffix,
|
||
const char *target_suffix) {
|
||
cbm_node_t *sources = NULL;
|
||
cbm_node_t *targets = NULL;
|
||
int source_count = 0;
|
||
int target_count = 0;
|
||
if (!store ||
|
||
cbm_store_find_nodes_by_qn_suffix(store, project, source_suffix, &sources, &source_count) !=
|
||
CBM_STORE_OK ||
|
||
cbm_store_find_nodes_by_qn_suffix(store, project, target_suffix, &targets, &target_count) !=
|
||
CBM_STORE_OK ||
|
||
source_count != 1 || target_count != 1) {
|
||
cbm_store_free_nodes(sources, source_count);
|
||
cbm_store_free_nodes(targets, target_count);
|
||
return -1;
|
||
}
|
||
cbm_edge_t *edges = NULL;
|
||
int edge_count = 0;
|
||
int matches = -1;
|
||
if (cbm_store_find_edges_by_source_type(store, sources[0].id, edge_type, &edges, &edge_count) ==
|
||
CBM_STORE_OK) {
|
||
matches = 0;
|
||
for (int i = 0; i < edge_count; i++) {
|
||
if (edges[i].target_id == targets[0].id) {
|
||
matches++;
|
||
}
|
||
}
|
||
}
|
||
cbm_store_free_edges(edges, edge_count);
|
||
cbm_store_free_nodes(sources, source_count);
|
||
cbm_store_free_nodes(targets, target_count);
|
||
return matches;
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 1: GO (lsp_cross WIRED) ──────────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* Go has the most mature lsp_cross. All 8 scenarios are expected GREEN.
|
||
* S1–S4 are same-package fixtures so the registry has full visibility.
|
||
* S5–S8 use two files to exercise cross-file type propagation.
|
||
*/
|
||
|
||
/* S1 — Go cross-file plain function call. */
|
||
TEST(lrp_go_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.go", "package app\n\nfunc Double(x int) int { return x * 2 }\n"},
|
||
{"main.go", "package app\n\nfunc Run(n int) int { return Double(n) }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — Go method dispatch via receiver type (cross-file). */
|
||
TEST(lrp_go_s2_method_dispatch) {
|
||
/* Counter is defined in counter.go; Run() in runner.go gets a Counter and
|
||
* calls its Inc() method. The lsp_cross pass sees Counter's type and
|
||
* resolves the method call to Counter.Inc. */
|
||
static const LRP_File f[] = {{"counter.go", "package app\n\ntype Counter struct{ n int }\n\n"
|
||
"func (c *Counter) Inc() { c.n++ }\n\n"
|
||
"func (c *Counter) Value() int { return c.n }\n"},
|
||
{"runner.go", "package app\n\nfunc Run(c *Counter) int {\n"
|
||
" c.Inc()\n return c.Value()\n}\n"}};
|
||
/* GREEN: Go lsp_cross resolves *Counter receiver → Inc and Value methods. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S2/method_dispatch", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — Go constructor / struct literal instantiation. */
|
||
TEST(lrp_go_s3_constructor) {
|
||
/* NewCounter() is a constructor-style function. The caller creates a Counter
|
||
* literal and calls NewCounter. Both the struct literal usage and the function
|
||
* call should produce CALLS edges. */
|
||
static const LRP_File f[] = {
|
||
{"counter.go",
|
||
"package app\n\ntype Counter struct{ start int }\n\n"
|
||
"func NewCounter(start int) *Counter {\n return &Counter{start: start}\n}\n"},
|
||
{"main.go", "package app\n\nfunc Make(n int) *Counter {\n return NewCounter(n)\n}\n"}};
|
||
/* GREEN: NewCounter is a top-level function; generic + lsp_cross both see it. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — Go package-qualified static call (pkg.Func). */
|
||
TEST(lrp_go_s4_static_call) {
|
||
/* Go's equivalent of a static call: importing another package and calling
|
||
* a top-level function via the package alias. */
|
||
static const LRP_File f[] = {
|
||
{"math/ops.go", "package math\n\nfunc Square(x int) int { return x * x }\n"},
|
||
{"main.go", "package main\n\nimport \"math\"\n\n"
|
||
"func Run(n int) int { return math.Square(n) }\n"}};
|
||
/* GREEN: lsp_cross maps import alias "math" → package QN, resolves math.Square. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S4/static_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — Go chained call (a.b().c()). */
|
||
TEST(lrp_go_s5_chained_call) {
|
||
/* builder.go defines a Builder with two chainable methods; main.go chains them.
|
||
* The second call (.Build()) is resolved via the return type of .Add() → *Builder. */
|
||
static const LRP_File f[] = {{"builder.go",
|
||
"package app\n\ntype Builder struct{ items []string }\n\n"
|
||
"func NewBuilder() *Builder { return &Builder{} }\n\n"
|
||
"func (b *Builder) Add(s string) *Builder {\n b.items = "
|
||
"append(b.items, s)\n return b\n}\n\n"
|
||
"func (b *Builder) Build() []string { return b.items }\n"},
|
||
{"main.go", "package app\n\nfunc Make() []string {\n"
|
||
" return NewBuilder().Add(\"x\").Build()\n}\n"}};
|
||
/* GREEN: lsp_cross infers *Builder from NewBuilder() return type, then
|
||
* resolves Add() → *Builder, then resolves Build(). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S5/chained_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — Go embedded-struct method call (Go's "inheritance"). */
|
||
TEST(lrp_go_s6_inherited_method) {
|
||
/* Dog embeds Animal. Calling dog.Speak() should resolve to Animal.Speak. */
|
||
static const LRP_File f[] = {{"animal.go",
|
||
"package zoo\n\ntype Animal struct{ Name string }\n\n"
|
||
"func (a *Animal) Speak() string { return a.Name }\n"},
|
||
{"dog.go", "package zoo\n\ntype Dog struct{ Animal }\n\n"
|
||
"func Run(d *Dog) string { return d.Speak() }\n"}};
|
||
/* GREEN: Go lsp_cross handles embedded-struct field promotion; Speak() is
|
||
* resolved to Animal.Speak via the embedded Animal field. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S6/inherited_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — Go generic function call (Go 1.18 generics). */
|
||
TEST(lrp_go_s7_generic_call) {
|
||
/* Map is a generic function in util.go; called from main.go with a concrete type. */
|
||
static const LRP_File f[] = {
|
||
{"util.go",
|
||
"package app\n\nfunc Map[T, U any](xs []T, fn func(T) U) []U {\n"
|
||
" out := make([]U, len(xs))\n"
|
||
" for i, x := range xs {\n out[i] = fn(x)\n }\n return out\n}\n"},
|
||
{"main.go", "package app\n\nfunc Double(xs []int) []int {\n"
|
||
" return Map(xs, func(x int) int { return x * 2 })\n}\n"}};
|
||
/* GREEN: Map is a top-level generic function; both generic + lsp_cross resolvers
|
||
* find it by name regardless of the type parameter instantiation. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S7/generic_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — Go field-type-hint (this.field.method()). */
|
||
TEST(lrp_go_s8_field_type_hint) {
|
||
/* Service has a field `repo *Repo`; its methods call repo.Find(). */
|
||
static const LRP_File f[] = {
|
||
{"repo.go", "package app\n\ntype Repo struct{}\n\n"
|
||
"func (r *Repo) Find(id int) string { return \"\" }\n"},
|
||
{"service.go", "package app\n\ntype Service struct{ repo *Repo }\n\n"
|
||
"func (s *Service) Get(id int) string { return s.repo.Find(id) }\n"}};
|
||
/* GREEN: lsp_cross sees s.repo field of type *Repo, resolves repo.Find. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S8/field_type_hint", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 2: C (lsp_cross WIRED) ───────────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* C has lsp_cross via cbm_run_c_lsp_cross. Function-pointer dispatch,
|
||
* struct-field calls, and pointer-to-function calls are the risky paths.
|
||
*/
|
||
|
||
/* S1 — C cross-file plain function call. */
|
||
TEST(lrp_c_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.c", "int add(int a, int b) { return a + b; }\n"},
|
||
{"main.c", "int add(int a, int b);\n\nint run(int x) { return add(x, 1); }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — C method-like call via function pointer in struct. */
|
||
TEST(lrp_c_s2_funcptr_in_struct) {
|
||
/* The C lsp_cross should detect obj.method() where method is a function
|
||
* pointer field with a known type. This is the C approximation of S2. */
|
||
static const LRP_File f[] = {
|
||
{"ops.c",
|
||
"typedef struct { int (*compute)(int); } Ops;\n\n"
|
||
"static int double_it(int x) { return x * 2; }\n\n"
|
||
"Ops make_ops(void) {\n Ops o;\n o.compute = double_it;\n return o;\n}\n"},
|
||
{"main.c", "typedef struct { int (*compute)(int); } Ops;\n"
|
||
"Ops make_ops(void);\n\n"
|
||
"int run(int n) {\n Ops o = make_ops();\n return o.compute(n);\n}\n"}};
|
||
/* Uncertain: C lsp_cross may not track function-pointer fields through a
|
||
* cross-file struct. Assert the CORRECT outcome; RED if unresolved. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S2/funcptr_in_struct", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — C "constructor" pattern (factory function returns pointer). */
|
||
TEST(lrp_c_s3_constructor) {
|
||
static const LRP_File f[] = {{"buf.c",
|
||
"#include <stdlib.h>\n"
|
||
"typedef struct { int cap; int *data; } Buf;\n\n"
|
||
"Buf *buf_new(int cap) {\n Buf *b = malloc(sizeof(Buf));\n"
|
||
" b->cap = cap;\n return b;\n}\n"},
|
||
{"main.c", "typedef struct { int cap; int *data; } Buf;\n"
|
||
"Buf *buf_new(int cap);\n\n"
|
||
"Buf *make_buf(int n) { return buf_new(n); }\n"}};
|
||
/* GREEN: buf_new is a plain function; both resolvers find it. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — C "static" pattern: file-scoped helper called across TUs. */
|
||
TEST(lrp_c_s4_static_local) {
|
||
/* Note: `static` in C means file-scoped — a static helper in util.c
|
||
* should NOT be resolvable from main.c. This probes that the resolver
|
||
* does NOT create a spurious cross-file CALLS edge for a static function.
|
||
* Correct outcome: calls == 0 from main.c to helper (it's not exported). */
|
||
static const LRP_File f[] = {
|
||
{"util.c", "static int helper(int x) { return x + 1; }\n\n"
|
||
"int exported(int x) { return helper(x); }\n"},
|
||
{"main.c", "int exported(int x);\n\nint run(int n) { return exported(n); }\n"}};
|
||
/* The CALLS edge run -> exported should exist.
|
||
* The CALLS edge anything -> helper (static) should NOT span files.
|
||
* We check that at least 1 CALLS edge resolves (run -> exported). */
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||
int calls = store ? cbm_store_count_edges_by_type(store, lp.project, "CALLS") : -1;
|
||
lrp_cleanup(&lp, store);
|
||
/* GREEN guard: exported() is a public function; run->exported resolves. */
|
||
ASSERT_TRUE(calls >= 1);
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — C chained dereference call (ptr->field->method). */
|
||
TEST(lrp_c_s5_chained_deref) {
|
||
static const LRP_File f[] = {
|
||
{"node.c", "typedef struct Node { int val; struct Node *next; } Node;\n\n"
|
||
"int node_val(const Node *n) { return n->val; }\n"},
|
||
{"main.c", "typedef struct Node { int val; struct Node *next; } Node;\n"
|
||
"int node_val(const Node *n);\n\n"
|
||
"int run(Node *head) { return node_val(head->next); }\n"}};
|
||
/* GREEN: node_val is a plain function; run->node_val resolves regardless. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S5/chained_deref", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — C "inheritance" pattern: base struct embedded at start. */
|
||
TEST(lrp_c_s6_base_method) {
|
||
static const LRP_File f[] = {
|
||
{"shape.c", "typedef struct { int color; } Shape;\n\n"
|
||
"int shape_color(const Shape *s) { return s->color; }\n"},
|
||
{"circle.c", "typedef struct { int color; } Shape;\n"
|
||
"typedef struct { Shape base; float radius; } Circle;\n"
|
||
"int shape_color(const Shape *s);\n\n"
|
||
"int run(Circle *c) { return shape_color(&c->base); }\n"}};
|
||
/* GREEN: shape_color is a plain function; run->shape_color resolves. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S6/base_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — C generic callback dispatch.
|
||
* FIXTURE FIX: the previous fixture only ASSIGNED int_cmp to a function pointer
|
||
* and then `(void)fn;` — it never invoked the callback, so there was no call to
|
||
* resolve (only a USAGE for the value reference). We now (a) invoke the callback
|
||
* through the pointer AND (b) call int_cmp directly cross-file (the same plain
|
||
* cross-file call shape that c/S1 resolves), so a CALLS edge is genuinely
|
||
* exercised. */
|
||
TEST(lrp_c_s7_generic_callback) {
|
||
static const LRP_File f[] = {{"compare.c", "int int_cmp(const void *a, const void *b) {\n"
|
||
" return *(const int*)a - *(const int*)b;\n}\n"},
|
||
{"sort.c", "typedef int (*CmpFn)(const void *, const void *);\n"
|
||
"int int_cmp(const void *a, const void *b);\n\n"
|
||
"int my_sort(int *arr, int n) {\n"
|
||
" CmpFn fn = int_cmp;\n"
|
||
" int direct = int_cmp(&arr[0], &arr[1]);\n"
|
||
" return direct + fn(&arr[0], &arr[1]);\n}\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S7/generic_callback", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — C field-type-hint: accessing a known struct member's address. */
|
||
TEST(lrp_c_s8_field_call) {
|
||
static const LRP_File f[] = {
|
||
{"logger.c", "typedef struct { int level; } Logger;\n\n"
|
||
"void log_msg(const Logger *l, const char *msg) { (void)l; (void)msg; }\n"},
|
||
{"service.c", "typedef struct { int level; } Logger;\n"
|
||
"typedef struct { Logger logger; int id; } Service;\n"
|
||
"void log_msg(const Logger *l, const char *msg);\n\n"
|
||
"void run(Service *svc, const char *m) { log_msg(&svc->logger, m); }\n"}};
|
||
/* GREEN: log_msg is a plain function; run->log_msg resolves. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S8/field_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 3: C++ (lsp_cross WIRED) ──────────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* C++ uses the same c_lsp_cross with cpp_mode=true. Virtual dispatch,
|
||
* templates, and namespace-qualified calls are the interesting paths.
|
||
*/
|
||
|
||
/* S1 — C++ cross-file plain function call. */
|
||
TEST(lrp_cpp_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.cpp", "int square(int x) { return x * x; }\n"},
|
||
{"main.cpp", "int square(int x);\n\nint run(int n) { return square(n); }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — C++ method dispatch on object (non-virtual). */
|
||
TEST(lrp_cpp_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"counter.cpp", "class Counter {\npublic:\n"
|
||
" int n;\n Counter() : n(0) {}\n"
|
||
" void inc() { n++; }\n int val() const { return n; }\n};\n"},
|
||
{"main.cpp", "class Counter;\n\n"
|
||
"int run() {\n Counter c;\n c.inc();\n return c.val();\n}\n"}};
|
||
/* Uncertain: forward-declaring a class without definition may limit lsp_cross
|
||
* type inference. Assert the correct outcome. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S2/method_dispatch", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — C++ constructor call. */
|
||
TEST(lrp_cpp_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"widget.cpp", "class Widget {\npublic:\n int id;\n"
|
||
" Widget(int id) : id(id) {}\n"
|
||
" int get_id() const { return id; }\n};\n"},
|
||
{"main.cpp", "class Widget { public: Widget(int); int get_id() const; };\n\n"
|
||
"int run(int n) {\n Widget w(n);\n return w.get_id();\n}\n"}};
|
||
/* GREEN: Widget(n) constructor call + get_id() method call. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — C++ static method call (Class::method). */
|
||
TEST(lrp_cpp_s4_static_method) {
|
||
static const LRP_File f[] = {{"registry.cpp", "class Registry {\npublic:\n"
|
||
" static int count() { return 42; }\n};\n"},
|
||
{"main.cpp", "class Registry { public: static int count(); };\n\n"
|
||
"int run() { return Registry::count(); }\n"}};
|
||
/* REAL BUG: a C++ static qualified call `Registry::count()` is not resolved to
|
||
* a CALLS edge by the C/C++ lsp_cross (cpp_mode) — diagnostics show calls=0 with
|
||
* the Registry method present (DEFINES_METHOD=1). The `Class::method()` static
|
||
* scope-resolution form is not handled by the C++ resolver in
|
||
* internal/cbm/lsp (cbm_run_c_lsp_cross, cpp path). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S4/static_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — C++ chained method call. */
|
||
TEST(lrp_cpp_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"builder.cpp", "class Builder {\npublic:\n"
|
||
" Builder& add(int x) { (void)x; return *this; }\n"
|
||
" int build() { return 1; }\n};\n"},
|
||
{"main.cpp", "class Builder { public: Builder& add(int); int build(); };\n\n"
|
||
"int run() { return Builder().add(1).build(); }\n"}};
|
||
/* GREEN: Builder() + add() + build() chain; lsp_cross resolves return types. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S5/chained", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — C++ virtual inherited method call. */
|
||
TEST(lrp_cpp_s6_virtual_inherited) {
|
||
static const LRP_File f[] = {
|
||
{"shape.cpp", "class Shape {\npublic:\n virtual int area() const { return 0; }\n};\n"},
|
||
{"circle.cpp", "class Shape { public: virtual int area() const; };\n\n"
|
||
"class Circle : public Shape {\npublic:\n"
|
||
" int r;\n Circle(int r) : r(r) {}\n"
|
||
" int area() const override { return r * r * 3; }\n"
|
||
" int run() const { return Shape::area(); }\n};\n"}};
|
||
/* REAL BUG: the explicit base call `Shape::area()` from Circle::run() is not
|
||
* resolved to a CALLS edge (diagnostics: calls=0, INHERITS=1 present). Same
|
||
* class as cpp/S4 — the C++ lsp_cross does not resolve `Base::method()`
|
||
* scope-qualified calls (internal/cbm/lsp, cbm_run_c_lsp_cross cpp path). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S6/virtual_inherited", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — C++ template function call. */
|
||
TEST(lrp_cpp_s7_template) {
|
||
static const LRP_File f[] = {
|
||
{"algo.cpp", "template<typename T>\nT clamp(T v, T lo, T hi) {\n"
|
||
" if (v < lo) return lo;\n if (v > hi) return hi;\n return v;\n}\n"},
|
||
{"main.cpp", "template<typename T> T clamp(T v, T lo, T hi);\n\n"
|
||
"int run(int n) { return clamp(n, 0, 100); }\n"}};
|
||
/* Uncertain: template functions require instantiation to resolve precisely.
|
||
* The name-based resolver may find clamp regardless; assert CALLS >= 1. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S7/template", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — C++ member field method call. */
|
||
TEST(lrp_cpp_s8_field_call) {
|
||
static const LRP_File f[] = {
|
||
{"logger.cpp",
|
||
"class Logger {\npublic:\n void log(const char *msg) { (void)msg; }\n};\n"},
|
||
{"service.cpp", "class Logger { public: void log(const char *); };\n\n"
|
||
"class Service {\n Logger logger;\npublic:\n"
|
||
" void run(const char *m) { logger.log(m); }\n};\n"}};
|
||
/* Uncertain: logger.log() — lsp_cross must see logger's type (Logger) from
|
||
* the Service class definition in the same file. Assert CALLS >= 1. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S8/field_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 4: RUST (lsp_cross NOT WIRED) ────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* cbm_run_rust_lsp_cross EXISTS in rust_lsp.c but cbm_pxc_has_cross_lsp
|
||
* returns false for CBM_LANG_RUST — the pass is NEVER called by the
|
||
* pipeline. The generic name-based resolver handles S1 (plain call).
|
||
* S2–S8 require type information and are expected RED (assert the correct
|
||
* outcome; they FAIL until cbm_pxc_has_cross_lsp is updated to include Rust).
|
||
*
|
||
* ROOT CAUSE: pass_lsp_cross.c cbm_pxc_has_cross_lsp() switch missing
|
||
* CBM_LANG_RUST case.
|
||
* FIX LOCATION: src/pipeline/pass_lsp_cross.c line ~280.
|
||
*/
|
||
|
||
/* S1 — Rust cross-file plain function call.
|
||
* REAL BUG (the known cross-LSP dispatch gap — keep RED): `lib::square(n)` does
|
||
* NOT resolve → calls=0. cbm_registry_resolve (src/pipeline/registry.c:638)
|
||
* splits the callee name on '.', not Rust's '::', so the prefix is the whole
|
||
* "lib::square" and never matches registered QN project.lib.square; and Rust
|
||
* lsp_cross is not wired (pass_lsp_cross.c:cbm_pxc_has_cross_lsp lacks
|
||
* CBM_LANG_RUST). Fix either the '::' split or wire Rust lsp_cross. */
|
||
TEST(lrp_rust_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"lib.rs", "pub fn square(x: i32) -> i32 { x * x }\n"},
|
||
{"main.rs", "mod lib;\n\nfn run(n: i32) -> i32 { lib::square(n) }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — Rust method dispatch via impl block (receiver type). */
|
||
TEST(lrp_rust_s2_method_dispatch) {
|
||
/* Counter is in counter.rs; run() in runner.rs calls c.inc() and c.value().
|
||
* Without lsp_cross, the receiver type Counter is unknown in runner.rs,
|
||
* so the method calls may not resolve to the correct impl methods. */
|
||
static const LRP_File f[] = {{"counter.rs", "pub struct Counter { n: i32 }\n\n"
|
||
"impl Counter {\n"
|
||
" pub fn new(n: i32) -> Self { Counter { n } }\n"
|
||
" pub fn inc(&mut self) { self.n += 1; }\n"
|
||
" pub fn value(&self) -> i32 { self.n }\n}\n"},
|
||
{"runner.rs",
|
||
"mod counter;\n\nfn run(c: &mut counter::Counter) -> i32 {\n"
|
||
" c.inc();\n c.value()\n}\n"}};
|
||
/* RED (expected to fail): lsp_cross not wired for Rust → method dispatch
|
||
* through typed receiver not resolved by the generic resolver.
|
||
* Root cause: cbm_pxc_has_cross_lsp returns false for CBM_LANG_RUST.
|
||
* This test asserts the CORRECT outcome: calls >= 1. FAILS until fixed. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S2/method_dispatch", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — Rust constructor (associated fn new). */
|
||
TEST(lrp_rust_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"point.rs",
|
||
"pub struct Point { pub x: f64, pub y: f64 }\n\n"
|
||
"impl Point {\n pub fn new(x: f64, y: f64) -> Self { Point { x, y } }\n"
|
||
" pub fn dist(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }\n}\n"},
|
||
{"main.rs", "mod point;\n\nfn run() -> f64 {\n"
|
||
" let p = point::Point::new(3.0, 4.0);\n p.dist()\n}\n"}};
|
||
/* RED: Point::new is a qualified associated function; the name resolver may find
|
||
* "new" generically, but Point::new in context requires lsp_cross.
|
||
* p.dist() requires receiver type Point — lsp_cross needed.
|
||
* Assert CALLS >= 1 (correct outcome). Fails until lsp_cross wired for Rust. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S3/constructor", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — Rust static / associated method call (Type::method). */
|
||
TEST(lrp_rust_s4_static_method) {
|
||
static const LRP_File f[] = {
|
||
{"config.rs", "pub struct Config { pub debug: bool }\n\n"
|
||
"impl Config {\n pub fn default() -> Self { Config { debug: false } }\n"
|
||
" pub fn is_debug(&self) -> bool { self.debug }\n}\n"},
|
||
{"app.rs", "mod config;\n\nfn run() -> bool {\n"
|
||
" let cfg = config::Config::default();\n cfg.is_debug()\n}\n"}};
|
||
/* RED: Config::default() is an associated (static-like) function.
|
||
* Without lsp_cross the receiver of is_debug() is unknown. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S4/static_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — Rust chained method call. */
|
||
TEST(lrp_rust_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"builder.rs", "pub struct Builder { items: Vec<i32> }\n\n"
|
||
"impl Builder {\n"
|
||
" pub fn new() -> Self { Builder { items: vec![] } }\n"
|
||
" pub fn add(mut self, x: i32) -> Self { self.items.push(x); self }\n"
|
||
" pub fn build(self) -> Vec<i32> { self.items }\n}\n"},
|
||
{"main.rs", "mod builder;\n\nfn run() -> Vec<i32> {\n"
|
||
" builder::Builder::new().add(1).add(2).build()\n}\n"}};
|
||
/* RED: the chain new() → add() → add() → build() requires lsp_cross to track
|
||
* that each add() returns Self (Builder), which is needed to resolve build(). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S5/chained", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — Rust trait method call on concrete type. */
|
||
TEST(lrp_rust_s6_trait_method) {
|
||
/* Trait Display is defined in display.rs; Dog implements it in dog.rs.
|
||
* run() in main.rs calls d.show() on a Dog where show() comes from the trait. */
|
||
static const LRP_File f[] = {
|
||
{"display.rs", "pub trait Display {\n fn show(&self) -> String;\n}\n"},
|
||
{"dog.rs", "mod display;\n\npub struct Dog { pub name: String }\n\n"
|
||
"impl display::Display for Dog {\n"
|
||
" fn show(&self) -> String { self.name.clone() }\n}\n"},
|
||
{"main.rs", "mod dog;\n\nfn run(d: &dog::Dog) -> String {\n d.show()\n}\n"}};
|
||
/* RED: d.show() on &Dog requires knowing Dog implements Display and that
|
||
* show() maps to Dog's impl — needs lsp_cross + trait resolution. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 3, 1, "rust/S6/trait_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — Rust generic function call. */
|
||
TEST(lrp_rust_s7_generic) {
|
||
static const LRP_File f[] = {
|
||
{"algo.rs", "pub fn max_of<T: PartialOrd>(a: T, b: T) -> T {\n"
|
||
" if a > b { a } else { b }\n}\n"},
|
||
{"main.rs", "mod algo;\n\nfn run(x: i32, y: i32) -> i32 {\n algo::max_of(x, y)\n}\n"}};
|
||
/* REAL BUG (same root cause as rust/S1): a Rust `::`-qualified cross-file path
|
||
* `algo::max_of(...)` is not resolved → calls=0. cbm_registry_resolve
|
||
* (src/pipeline/registry.c:638) splits the callee on '.', not '::', so the
|
||
* prefix becomes the whole "algo::max_of" and never matches the registered QN
|
||
* (project.algo.max_of); Rust lsp_cross is also not wired
|
||
* (pass_lsp_cross.c:cbm_pxc_has_cross_lsp lacks CBM_LANG_RUST). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S7/generic", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — Rust field method call. */
|
||
TEST(lrp_rust_s8_field_call) {
|
||
static const LRP_File f[] = {
|
||
{"logger.rs", "pub struct Logger;\n\nimpl Logger {\n"
|
||
" pub fn log(&self, msg: &str) { let _ = msg; }\n}\n"},
|
||
{"service.rs", "mod logger;\n\npub struct Service { pub logger: logger::Logger }\n\n"
|
||
"impl Service {\n"
|
||
" pub fn run(&self, msg: &str) { self.logger.log(msg); }\n}\n"}};
|
||
/* RED: self.logger.log() — lsp_cross must see that self.logger is of type
|
||
* logger::Logger and resolve log() to Logger::log.
|
||
* Without lsp_cross the receiver type is unknown. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S8/field_call", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* Cross-file call hidden inside format!'s token tree. The visible() control
|
||
* proves the target and ordinary cross-file resolution are present; hidden()
|
||
* requires the Rust LSP's semantic result to be paired with a synthetic
|
||
* CBMCall carrier before pass_calls can materialize the exact graph edge. */
|
||
TEST(lrp_rust_crossfile_macro_hidden_call_carrier) {
|
||
static const LRP_File f[] = {
|
||
{"lib.rs", "pub fn render() -> &'static str { \"ok\" }\n"},
|
||
{"main.rs", "mod lib;\n\n"
|
||
"pub fn visible() -> &'static str { lib::render() }\n\n"
|
||
"pub fn hidden() -> String { format!(\"{}\", lib::render()) }\n"}};
|
||
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||
int visible_to_render = lrp_exact_calls_by_name(store, lp.project, "visible", "render");
|
||
int hidden_to_render = lrp_exact_calls_by_name(store, lp.project, "hidden", "render");
|
||
if (visible_to_render != 1 || hidden_to_render != 1) {
|
||
fprintf(stderr,
|
||
" [LRP] rust/macro_hidden_carrier visible->render=%d "
|
||
"hidden->render=%d (expected 1 each)\n",
|
||
visible_to_render, hidden_to_render);
|
||
lrp_diag(store, lp.project, "rust/macro_hidden_carrier");
|
||
}
|
||
lrp_cleanup(&lp, store);
|
||
|
||
ASSERT_EQ(visible_to_render, 1); /* ordinary-call control */
|
||
ASSERT_EQ(hidden_to_render, 1); /* synthetic carrier regression guard */
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 5: PYTHON (lsp_cross WIRED) ──────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* Python has cbm_run_py_lsp_cross. Type inference is limited by duck
|
||
* typing, but explicit type annotations (PEP 484) help the LSP.
|
||
*/
|
||
|
||
/* S1 — Python cross-file plain function call. */
|
||
TEST(lrp_python_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.py", "def triple(x):\n return x * 3\n"},
|
||
{"main.py", "from .util import triple\n\n\ndef run(n):\n return triple(n)\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — Python method dispatch via known class. */
|
||
TEST(lrp_python_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"counter.py", "class Counter:\n"
|
||
" def __init__(self):\n self.n = 0\n\n"
|
||
" def inc(self):\n self.n += 1\n\n"
|
||
" def value(self):\n return self.n\n"},
|
||
{"main.py", "from .counter import Counter\n\n\n"
|
||
"def run():\n c = Counter()\n c.inc()\n return c.value()\n"}};
|
||
/* GREEN: py_lsp_cross tracks c = Counter() → type Counter; resolves c.inc()
|
||
* and c.value() to Counter.inc / Counter.value. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S2/method_dispatch", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — Python constructor call (class instantiation). */
|
||
TEST(lrp_python_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"widget.py", "class Widget:\n def __init__(self, name):\n self.name = name\n\n"
|
||
" def label(self):\n return self.name\n"},
|
||
{"main.py", "from .widget import Widget\n\n\ndef make(name):\n return Widget(name)\n"}};
|
||
/* GREEN: Widget(name) is a constructor call; py_lsp_cross sees Widget type from
|
||
* the import and creates a CALLS edge make -> Widget.__init__. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — Python class method call (classmethod / staticmethod). */
|
||
TEST(lrp_python_s4_class_method) {
|
||
static const LRP_File f[] = {
|
||
{"factory.py", "class Factory:\n"
|
||
" @classmethod\n def create(cls, n):\n return cls()\n\n"
|
||
" @staticmethod\n def version():\n return '1.0'\n"},
|
||
{"main.py", "from .factory import Factory\n\n\ndef run():\n"
|
||
" v = Factory.version()\n return v\n"}};
|
||
/* GREEN: Factory.version() is a static method call; lsp_cross resolves it. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S4/class_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — Python chained call. */
|
||
TEST(lrp_python_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"builder.py",
|
||
"class Builder:\n def __init__(self):\n self.items = []\n\n"
|
||
" def add(self, x):\n self.items.append(x)\n return self\n\n"
|
||
" def build(self):\n return list(self.items)\n"},
|
||
{"main.py", "from .builder import Builder\n\n\ndef make():\n"
|
||
" return Builder().add(1).add(2).build()\n"}};
|
||
/* GREEN: Builder() returns Builder; add() returns self (Builder); build()
|
||
* resolves via return-type chain. py_lsp_cross handles self-returning methods. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S5/chained", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — Python inherited method call. */
|
||
TEST(lrp_python_s6_inherited_method) {
|
||
static const LRP_File f[] = {
|
||
{"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):\n return c.describe()\n"}};
|
||
/* 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. */
|
||
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");
|
||
lrp_cleanup(&lp, store);
|
||
ASSERT_EQ(calls, 0);
|
||
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[] = {
|
||
{"repo.py", "class Repo:\n def find(self, id: int) -> str:\n return ''\n"},
|
||
{"service.py", "from .repo import Repo\n\n\nclass Service:\n"
|
||
" def __init__(self, repo: Repo):\n self.repo = repo\n\n"
|
||
" def get(self, id: int) -> str:\n return self.repo.find(id)\n"}};
|
||
/* GREEN: self.repo is annotated as Repo; py_lsp_cross reads the annotation
|
||
* and resolves self.repo.find() to Repo.find. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S7/annotated_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — Python field-type-hint (self.field.method()). */
|
||
TEST(lrp_python_s8_field_type_hint) {
|
||
/* Service stores a logger field and calls logger.log() in methods. */
|
||
static const LRP_File f[] = {
|
||
{"logger.py", "class Logger:\n def log(self, msg: str) -> None:\n pass\n"},
|
||
{"service.py", "from .logger import Logger\n\n\nclass Service:\n"
|
||
" def __init__(self):\n self.logger: Logger = Logger()\n\n"
|
||
" def run(self, msg: str):\n self.logger.log(msg)\n"}};
|
||
/* GREEN: self.logger is annotated as Logger; py_lsp_cross resolves
|
||
* self.logger.log() to Logger.log. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S8/field_type_hint", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* A cross-file dunder can resolve semantically only after the project-wide
|
||
* Python registry is available. Because `left + right` is not a parser call,
|
||
* the cross resolver must return an exact synthetic carrier as well as the
|
||
* semantic record or pass_calls has nothing to materialize. */
|
||
TEST(lrp_python_crossfile_dunder_carrier) {
|
||
static const LRP_File f[] = {{"alpha.py", "class Alpha:\n"
|
||
" def __add__(self, other):\n"
|
||
" return self\n"},
|
||
{"main.py", "from .alpha import Alpha\n\n"
|
||
"def combine(left: Alpha, right: Alpha):\n"
|
||
" return left + right\n"}};
|
||
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||
int combine_to_add = lrp_exact_calls_by_name(store, lp.project, "combine", "__add__");
|
||
if (combine_to_add != 1) {
|
||
fprintf(stderr,
|
||
" [LRP] python/crossfile_dunder_carrier combine->__add__=%d "
|
||
"(expected 1)\n",
|
||
combine_to_add);
|
||
lrp_diag(store, lp.project, "python/crossfile_dunder_carrier");
|
||
}
|
||
lrp_cleanup(&lp, store);
|
||
|
||
ASSERT_EQ(combine_to_add, 1);
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 6: TYPESCRIPT (lsp_cross WIRED) ───────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* TypeScript has cbm_run_ts_lsp_cross. Static types make S2–S8 more
|
||
* tractable than Python.
|
||
*/
|
||
|
||
/* S1 — TypeScript cross-file plain function call. */
|
||
TEST(lrp_ts_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.ts", "export function format(s: string): string { return s.trim(); }\n"},
|
||
{"main.ts", "import { format } from './util';\n\n"
|
||
"export function run(s: string): string { return format(s); }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — TypeScript method dispatch via typed variable. */
|
||
TEST(lrp_ts_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"counter.ts",
|
||
"export class Counter {\n private n = 0;\n"
|
||
" inc(): void { this.n++; }\n value(): number { return this.n; }\n}\n"},
|
||
{"main.ts", "import { Counter } from './counter';\n\n"
|
||
"export function run(): number {\n"
|
||
" const c = new Counter();\n c.inc();\n return c.value();\n}\n"}};
|
||
/* GREEN: ts_lsp_cross resolves c: Counter → inc() and value(). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S2/method_dispatch", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — TypeScript constructor call. */
|
||
TEST(lrp_ts_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"widget.ts", "export class Widget {\n constructor(public name: string) {}\n"
|
||
" label(): string { return this.name; }\n}\n"},
|
||
{"main.ts",
|
||
"import { Widget } from './widget';\n\n"
|
||
"export function make(name: string): Widget {\n return new Widget(name);\n}\n"}};
|
||
/* REAL BUG: `new Widget(name)` is extracted (new_expression IS in js_call_types)
|
||
* but produces NO CALLS edge — diagnostics show calls=0 with the Widget
|
||
* constructor present (DEFINES_METHOD=2, IMPORTS=1). The TS resolver does not
|
||
* link a `new T()` instantiation to the class/constructor node (cf. Python S3,
|
||
* which DOES route Widget(name) → __init__ and passes). Compare the WIRED
|
||
* Python constructor path in internal/cbm/lsp vs the TS new_expression path. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — TypeScript static method call. */
|
||
TEST(lrp_ts_s4_static_method) {
|
||
static const LRP_File f[] = {
|
||
{"factory.ts", "export class Factory {\n static version(): string { return '1.0'; }\n"
|
||
" static create(): Factory { return new Factory(); }\n}\n"},
|
||
{"main.ts", "import { Factory } from './factory';\n\n"
|
||
"export function run(): string { return Factory.version(); }\n"}};
|
||
/* GREEN: Factory.version() is a static method; ts_lsp_cross resolves it via
|
||
* the imported Factory class. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S4/static_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — TypeScript chained method call. */
|
||
TEST(lrp_ts_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"builder.ts", "export class Builder {\n private items: number[] = [];\n"
|
||
" add(x: number): Builder { this.items.push(x); return this; }\n"
|
||
" build(): number[] { return this.items; }\n}\n"},
|
||
{"main.ts", "import { Builder } from './builder';\n\n"
|
||
"export function run(): number[] {\n"
|
||
" return new Builder().add(1).add(2).build();\n}\n"}};
|
||
/* GREEN: ts_lsp_cross infers Builder from new Builder(), add() returns Builder,
|
||
* build() resolves through the chain. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S5/chained", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — TypeScript inherited method call. */
|
||
TEST(lrp_ts_s6_inherited_method) {
|
||
static const LRP_File f[] = {
|
||
{"base.ts", "export class Base {\n describe(): string { return 'base'; }\n}\n"},
|
||
{"child.ts",
|
||
"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"}};
|
||
/* 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_TRUE(calls >= 1);
|
||
ASSERT_TRUE(lsp_calls >= 1);
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — TypeScript generic function call. */
|
||
TEST(lrp_ts_s7_generic) {
|
||
static const LRP_File f[] = {
|
||
{"algo.ts", "export function maxOf<T>(a: T, b: T, cmp: (x: T, y: T) => number): T {\n"
|
||
" return cmp(a, b) >= 0 ? a : b;\n}\n"},
|
||
{"main.ts", "import { maxOf } from './algo';\n\n"
|
||
"export function run(a: number, b: number): number {\n"
|
||
" return maxOf(a, b, (x, y) => x - y);\n}\n"}};
|
||
/* GREEN: maxOf is a named import; ts_lsp_cross resolves it regardless of
|
||
* type parameter instantiation. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S7/generic", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — TypeScript field-type-hint (this.field.method()). */
|
||
TEST(lrp_ts_s8_field_type_hint) {
|
||
static const LRP_File f[] = {
|
||
{"logger.ts",
|
||
"export class Logger {\n log(msg: string): void { console.log(msg); }\n}\n"},
|
||
{"service.ts", "import { Logger } from './logger';\n\n"
|
||
"export class Service {\n private logger: Logger;\n"
|
||
" constructor() { this.logger = new Logger(); }\n"
|
||
" run(msg: string): void { this.logger.log(msg); }\n}\n"}};
|
||
/* GREEN: this.logger is typed as Logger; ts_lsp_cross sees the field type
|
||
* and resolves this.logger.log() to Logger.log. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S8/field_type_hint", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 7: JAVA (lsp_cross NOT WIRED) ─────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* cbm_run_java_lsp_cross EXISTS (java_lsp.h) but cbm_pxc_has_cross_lsp
|
||
* returns false for CBM_LANG_JAVA.
|
||
* S1 (plain call via Util.method()): GREEN via name-based resolver.
|
||
* S2–S8: RED until cbm_pxc_has_cross_lsp is updated.
|
||
*
|
||
* ROOT CAUSE: pass_lsp_cross.c cbm_pxc_has_cross_lsp() missing CBM_LANG_JAVA.
|
||
* FIX LOCATION: src/pipeline/pass_lsp_cross.c line ~280.
|
||
*/
|
||
|
||
/* S1 — Java cross-file plain method call (same package). */
|
||
TEST(lrp_java_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"Util.java",
|
||
"package app;\n\nclass Util {\n static int square(int x) { return x * x; }\n}\n"},
|
||
{"Main.java",
|
||
"package app;\n\nclass Main {\n int run(int n) { return Util.square(n); }\n}\n"}};
|
||
/* GREEN: Util.square is resolved by the name-based resolver (class.method name match). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — Java method dispatch on instance (receiver type from field). */
|
||
TEST(lrp_java_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"Counter.java",
|
||
"package app;\n\nclass Counter {\n private int n = 0;\n"
|
||
" public void inc() { n++; }\n public int value() { return n; }\n}\n"},
|
||
{"Runner.java", "package app;\n\nclass Runner {\n"
|
||
" public int run() {\n Counter c = new Counter();\n"
|
||
" c.inc();\n return c.value();\n }\n}\n"}};
|
||
/* RED: c.inc() / c.value() on Counter — without java_lsp_cross the
|
||
* receiver type (Counter) is not known; the name-based resolver may find
|
||
* "inc" and "value" by name, but won't guarantee the correct attribution.
|
||
* Assert the correct outcome (CALLS >= 1). Fails until lsp_cross wired. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S2/method_dispatch", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — Java constructor call (new T()). */
|
||
TEST(lrp_java_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"Widget.java", "package app;\n\nclass Widget {\n private String name;\n"
|
||
" public Widget(String name) { this.name = name; }\n"
|
||
" public String label() { return name; }\n}\n"},
|
||
{"Factory.java", "package app;\n\nclass Factory {\n"
|
||
" public Widget make(String name) { return new Widget(name); }\n}\n"}};
|
||
/* REAL BUG: `new Widget(name)` yields NO CALLS edge (diagnostics: calls=0,
|
||
* DEFINES_METHOD=3 present). ROOT CAUSE: java_call_types (lang_specs.c) is
|
||
* {"method_invocation"} only — it does NOT include
|
||
* "object_creation_expression", so a Java `new T()` is never even extracted as
|
||
* a call. Add object_creation_expression to java_call_types (+ route to the
|
||
* constructor) to fix. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S3/constructor", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — Java static method call (Class.staticMethod). */
|
||
TEST(lrp_java_s4_static_method) {
|
||
static const LRP_File f[] = {
|
||
{"MathUtil.java", "package app;\n\nclass MathUtil {\n"
|
||
" public static int clamp(int v, int lo, int hi) {\n"
|
||
" return v < lo ? lo : v > hi ? hi : v;\n }\n}\n"},
|
||
{"App.java", "package app;\n\nclass App {\n"
|
||
" public int run(int n) { return MathUtil.clamp(n, 0, 100); }\n}\n"}};
|
||
/* Uncertain: MathUtil.clamp — the name resolver may find "clamp" via name match.
|
||
* Assert CALLS >= 1; may be GREEN even without lsp_cross if name is unique. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S4/static_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — Java chained method call. */
|
||
TEST(lrp_java_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"Builder.java", "package app;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n"
|
||
"class Builder {\n private List<Integer> items = new ArrayList<>();\n"
|
||
" public Builder add(int x) { items.add(x); return this; }\n"
|
||
" public List<Integer> build() { return items; }\n}\n"},
|
||
{"Main.java", "package app;\n\nimport java.util.List;\n\n"
|
||
"class Main {\n public List<Integer> run() {\n"
|
||
" return new Builder().add(1).add(2).build();\n }\n}\n"}};
|
||
/* RED: chained calls new Builder().add(1).add(2).build() require type tracking.
|
||
* Without lsp_cross the intermediate types are unknown. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S5/chained", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — Java inherited method call (subclass calls base method). */
|
||
TEST(lrp_java_s6_inherited_method) {
|
||
static const LRP_File f[] = {{"Animal.java", "package zoo;\n\nclass Animal {\n public "
|
||
"String describe() { return \"animal\"; }\n}\n"},
|
||
{"Dog.java",
|
||
"package zoo;\n\nclass Dog extends Animal {\n"
|
||
" public String run() { return describe(); }\n}\n"}};
|
||
/* Uncertain: describe() in Dog.run() — the name-based resolver may find it
|
||
* if "describe" is unique in the project. Assert CALLS >= 1. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S6/inherited_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — Java generic method call. */
|
||
TEST(lrp_java_s7_generic) {
|
||
static const LRP_File f[] = {
|
||
{"Util.java", "package app;\n\nimport java.util.List;\n\n"
|
||
"class Util {\n public static <T> T first(List<T> list) {\n"
|
||
" return list.isEmpty() ? null : list.get(0);\n }\n}\n"},
|
||
{"App.java", "package app;\n\nimport java.util.List;\nimport java.util.ArrayList;\n\n"
|
||
"class App {\n public String run() {\n"
|
||
" List<String> xs = new ArrayList<>();\n"
|
||
" xs.add(\"hello\");\n return Util.first(xs);\n }\n}\n"}};
|
||
/* Uncertain: Util.first is a generic static; name resolver may find it. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S7/generic", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — Java field-type-hint (this.field.method()). */
|
||
TEST(lrp_java_s8_field_type_hint) {
|
||
static const LRP_File f[] = {
|
||
{"Logger.java", "package app;\n\nclass Logger {\n"
|
||
" public void log(String msg) { System.out.println(msg); }\n}\n"},
|
||
{"Service.java",
|
||
"package app;\n\nclass Service {\n private Logger logger = new Logger();\n"
|
||
" public void run(String msg) { logger.log(msg); }\n}\n"}};
|
||
/* RED: logger.log() — without lsp_cross the receiver type Logger is not
|
||
* tracked from the field declaration across to the call site. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S8/field_type_hint", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 8: KOTLIN (lsp_cross NOT IMPLEMENTED) ─────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* Kotlin has cbm_run_kotlin_lsp (per-file) but NO cbm_run_kotlin_lsp_cross.
|
||
* The generic name-based resolver covers S1. S2–S8 are RED.
|
||
*
|
||
* ROOT CAUSE: No cross-file LSP function exists for Kotlin; the per-file
|
||
* cbm_run_kotlin_lsp is never invoked with cross-file project-wide defs.
|
||
* FIX LOCATION: internal/cbm/lsp/kotlin_lsp.c — add cbm_run_kotlin_lsp_cross,
|
||
* then wire it in pass_lsp_cross.c.
|
||
*/
|
||
|
||
/* S1 — Kotlin cross-file plain function call.
|
||
* REAL BUG (the known Kotlin cross-LSP gap — keep RED): even a bare top-level
|
||
* `double(n)` does not resolve cross-file → calls=0 (diagnostics: only DEFINES=4).
|
||
* Kotlin has no cbm_run_kotlin_lsp_cross and the generic name resolver does not
|
||
* link this Kotlin cross-file call. Fix = add Kotlin cross-file LSP
|
||
* (internal/cbm/lsp/kotlin_lsp.c) and wire it in pass_lsp_cross.c. */
|
||
TEST(lrp_kotlin_s1_crossfile_call) {
|
||
static const LRP_File f[] = {{"Util.kt", "fun double(x: Int): Int = x * 2\n"},
|
||
{"Main.kt", "fun run(n: Int): Int = double(n)\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — Kotlin method dispatch on typed receiver. */
|
||
TEST(lrp_kotlin_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"Counter.kt", "class Counter {\n private var n = 0\n"
|
||
" fun inc() { n++ }\n fun value(): Int = n\n}\n"},
|
||
{"Runner.kt", "fun run(c: Counter): Int {\n c.inc()\n return c.value()\n}\n"}};
|
||
/* RED: c.inc() / c.value() require knowing c: Counter and resolving
|
||
* methods through the class definition in Counter.kt. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S2/method_dispatch", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — Kotlin constructor call. */
|
||
TEST(lrp_kotlin_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"Widget.kt", "class Widget(val name: String) {\n fun label(): String = name\n}\n"},
|
||
{"Main.kt", "fun make(name: String): Widget = Widget(name)\n"}};
|
||
/* RED: Widget(name) is a constructor call; without cross-file LSP the type
|
||
* Widget from Widget.kt is not in the per-file resolver scope. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S3/constructor", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — Kotlin companion-object static call. */
|
||
TEST(lrp_kotlin_s4_companion_static) {
|
||
static const LRP_File f[] = {
|
||
{"Config.kt",
|
||
"class Config(val debug: Boolean) {\n"
|
||
" companion object {\n fun default(): Config = Config(false)\n }\n}\n"},
|
||
{"App.kt", "fun run(): Config = Config.default()\n"}};
|
||
/* RED: Config.default() is a companion-object call; requires cross-file type
|
||
* knowledge of Config and its companion object. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S4/companion_static", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — Kotlin chained method call. */
|
||
TEST(lrp_kotlin_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"Builder.kt", "class Builder {\n private val items = mutableListOf<Int>()\n"
|
||
" fun add(x: Int): Builder { items.add(x); return this }\n"
|
||
" fun build(): List<Int> = items.toList()\n}\n"},
|
||
{"Main.kt", "fun run(): List<Int> = Builder().add(1).add(2).build()\n"}};
|
||
/* RED: Builder() → add() → build() chain requires type tracking across files. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S5/chained", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — Kotlin inherited method call (open class). */
|
||
TEST(lrp_kotlin_s6_inherited_method) {
|
||
static const LRP_File f[] = {
|
||
{"Base.kt", "open class Base {\n open fun describe(): String = \"base\"\n}\n"},
|
||
{"Child.kt", "class Child : Base() {\n fun run(): String = describe()\n}\n"}};
|
||
/* RED: describe() in Child.run() requires knowing Child extends Base.
|
||
* Kotlin extraction bug (`:` supertype not parsed) compounds this. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S6/inherited_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — Kotlin generic function call. */
|
||
TEST(lrp_kotlin_s7_generic) {
|
||
static const LRP_File f[] = {
|
||
{"Algo.kt", "fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b\n"},
|
||
{"Main.kt", "fun run(a: Int, b: Int): Int = maxOf(a, b)\n"}};
|
||
/* Uncertain: maxOf is a top-level generic function; name resolver may find
|
||
* it. Assert CALLS >= 1; may be GREEN via name resolver. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S7/generic", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — Kotlin field-type-hint (this.field.method()). */
|
||
TEST(lrp_kotlin_s8_field_type_hint) {
|
||
static const LRP_File f[] = {
|
||
{"Logger.kt", "class Logger {\n fun log(msg: String): Unit { println(msg) }\n}\n"},
|
||
{"Service.kt", "class Service {\n private val logger = Logger()\n"
|
||
" fun run(msg: String) { logger.log(msg) }\n}\n"}};
|
||
/* RED: logger.log() — without cross-file LSP the type of logger (Logger)
|
||
* is not known; name resolver may find "log" by name alone. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S8/field_type_hint", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── GROUP 9: C# (lsp_cross NOT WIRED) ───────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* cbm_run_cs_lsp_cross EXISTS (cs_lsp.c) and the CBMCrossLspRegistries
|
||
* struct even has a `cs` field — but cbm_pxc_has_cross_lsp returns false
|
||
* for CBM_LANG_CSHARP. The cross-LSP pass is never invoked for C# files.
|
||
*
|
||
* ROOT CAUSE: pass_lsp_cross.c cbm_pxc_has_cross_lsp() missing CBM_LANG_CSHARP.
|
||
* FIX LOCATION: src/pipeline/pass_lsp_cross.c line ~280.
|
||
* NOTE: CBMCrossLspRegistries.cs is already defined and cbm_pxc_registry_for_lang
|
||
* handles CSHARP — so the fix is a 1-line addition to cbm_pxc_has_cross_lsp().
|
||
*/
|
||
|
||
/* S1 — C# cross-file plain static call. */
|
||
TEST(lrp_csharp_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"Util.cs", "namespace App {\n class Util {\n"
|
||
" public static int Square(int x) { return x * x; }\n }\n}\n"},
|
||
{"Main.cs", "namespace App {\n class Main {\n"
|
||
" public int Run(int n) { return Util.Square(n); }\n }\n}\n"}};
|
||
/* GREEN: Util.Square found by name-based resolver (class.method pattern). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — C# method dispatch on typed instance. */
|
||
TEST(lrp_csharp_s2_method_dispatch) {
|
||
static const LRP_File f[] = {
|
||
{"Counter.cs", "namespace App {\n class Counter {\n private int n = 0;\n"
|
||
" public void Inc() { n++; }\n public int Value() { return n; "
|
||
"}\n }\n}\n"},
|
||
{"Runner.cs",
|
||
"namespace App {\n class Runner {\n"
|
||
" public int Run() {\n var c = new Counter();\n"
|
||
" c.Inc();\n return c.Value();\n }\n }\n}\n"}};
|
||
/* RED: c.Inc() / c.Value() on Counter — without cs_lsp_cross the receiver
|
||
* type Counter is not tracked from new Counter() to the call site. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S2/method_dispatch", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — C# constructor call (new T()). */
|
||
TEST(lrp_csharp_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"Widget.cs", "namespace App {\n class Widget {\n public string Name;\n"
|
||
" public Widget(string name) { Name = name; }\n"
|
||
" public string Label() { return Name; }\n }\n}\n"},
|
||
{"Factory.cs",
|
||
"namespace App {\n class Factory {\n"
|
||
" public Widget Make(string name) { return new Widget(name); }\n }\n}\n"}};
|
||
/* REAL BUG: `new Widget(name)` yields NO CALLS edge (diagnostics: calls=0,
|
||
* DEFINES_METHOD=3 present). ROOT CAUSE: cs_call_types (lang_specs.c) is
|
||
* {"invocation_expression"} only — it omits "object_creation_expression", so a
|
||
* C# `new T()` is never extracted as a call. (All other C# scenarios
|
||
* S2/S4–S8 unexpectedly PASS, confirming C# resolution itself works; only the
|
||
* constructor extraction is missing.) */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S3/constructor", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — C# static method call. */
|
||
TEST(lrp_csharp_s4_static_method) {
|
||
static const LRP_File f[] = {
|
||
{"MathHelper.cs",
|
||
"namespace App {\n class MathHelper {\n"
|
||
" public static int Clamp(int v, int lo, int hi) {\n"
|
||
" return v < lo ? lo : v > hi ? hi : v;\n }\n }\n}\n"},
|
||
{"App.cs",
|
||
"namespace App {\n class AppEntry {\n"
|
||
" public int Run(int n) { return MathHelper.Clamp(n, 0, 100); }\n }\n}\n"}};
|
||
/* Uncertain: MathHelper.Clamp — name resolver may find "Clamp" if unique. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S4/static_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — C# chained method call. */
|
||
TEST(lrp_csharp_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"Builder.cs", "namespace App {\n using System.Collections.Generic;\n"
|
||
" class Builder {\n private List<int> items = new List<int>();\n"
|
||
" public Builder Add(int x) { items.Add(x); return this; }\n"
|
||
" public List<int> Build() { return items; }\n }\n}\n"},
|
||
{"Main.cs",
|
||
"namespace App {\n using System.Collections.Generic;\n"
|
||
" class Main {\n public List<int> Run() {\n"
|
||
" return new Builder().Add(1).Add(2).Build();\n }\n }\n}\n"}};
|
||
/* RED: chained calls new Builder().Add(1).Add(2).Build() — type tracking needed. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S5/chained", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — C# inherited method call. */
|
||
TEST(lrp_csharp_s6_inherited_method) {
|
||
static const LRP_File f[] = {
|
||
{"Base.cs", "namespace App {\n class Base {\n"
|
||
" public virtual string Describe() { return \"base\"; }\n }\n}\n"},
|
||
{"Child.cs", "namespace App {\n class Child : Base {\n"
|
||
" public string Run() { return Describe(); }\n }\n}\n"}};
|
||
/* Uncertain: Describe() in Child.Run() — name resolver may find it if unique. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S6/inherited_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — C# generic method call. */
|
||
TEST(lrp_csharp_s7_generic) {
|
||
static const LRP_File f[] = {
|
||
{"Container.cs",
|
||
"namespace App {\n using System.Collections.Generic;\n"
|
||
" class Container {\n"
|
||
" public static T First<T>(List<T> xs) {\n"
|
||
" return xs.Count > 0 ? xs[0] : default;\n }\n }\n}\n"},
|
||
{"App.cs", "namespace App {\n using System.Collections.Generic;\n"
|
||
" class AppEntry {\n"
|
||
" public string Run() {\n"
|
||
" var xs = new List<string> { \"hello\" };\n"
|
||
" return Container.First(xs);\n }\n }\n}\n"}};
|
||
/* Uncertain: Container.First is a static generic method. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S7/generic", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — C# field-type-hint (this.field.method()). */
|
||
TEST(lrp_csharp_s8_field_type_hint) {
|
||
static const LRP_File f[] = {
|
||
{"Logger.cs", "namespace App {\n class Logger {\n"
|
||
" public void Log(string msg) { }\n }\n}\n"},
|
||
{"Service.cs", "namespace App {\n class Service {\n"
|
||
" private Logger logger = new Logger();\n"
|
||
" public void Run(string msg) { logger.Log(msg); }\n }\n}\n"}};
|
||
/* RED: logger.Log() — without cs_lsp_cross the receiver type Logger is not
|
||
* tracked from the field declaration. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S8/field_type_hint", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── BONUS: PHP (lsp_cross WIRED) ────────────────────────────────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* PHP has cbm_run_php_lsp_cross. Key scenarios: $this->method(),
|
||
* new T(), static T::method(), chaining.
|
||
*/
|
||
|
||
/* S1 — PHP cross-file function call. */
|
||
TEST(lrp_php_s1_crossfile_call) {
|
||
static const LRP_File f[] = {
|
||
{"util.php", "<?php\nfunction triple($x) { return $x * 3; }\n"},
|
||
{"main.php", "<?php\nrequire_once 'util.php';\nfunction run($n) { return triple($n); }\n"}};
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S1/crossfile_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S2 — PHP method dispatch via $this (cross-file). */
|
||
TEST(lrp_php_s2_method_dispatch) {
|
||
static const LRP_File f[] = {{"Counter.php",
|
||
"<?php\nclass Counter {\n private $n = 0;\n"
|
||
" public function inc() { $this->n++; }\n"
|
||
" public function value() { return $this->n; }\n}\n"},
|
||
{"main.php", "<?php\nrequire_once 'Counter.php';\n"
|
||
"function run() {\n $c = new Counter();\n"
|
||
" $c->inc();\n return $c->value();\n}\n"}};
|
||
/* GREEN: php_lsp_cross tracks $c = new Counter() → type Counter;
|
||
* resolves $c->inc() / $c->value(). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S2/method_dispatch", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S3 — PHP constructor (new T()). */
|
||
TEST(lrp_php_s3_constructor) {
|
||
static const LRP_File f[] = {
|
||
{"Widget.php", "<?php\nclass Widget {\n public $name;\n"
|
||
" public function __construct($name) { $this->name = $name; }\n"
|
||
" public function label() { return $this->name; }\n}\n"},
|
||
{"factory.php", "<?php\nrequire_once 'Widget.php';\n"
|
||
"function make($name) { return new Widget($name); }\n"}};
|
||
/* REAL BUG: `new Widget($name)` yields NO CALLS edge (diagnostics: calls=0,
|
||
* DEFINES_METHOD=2 present). ROOT CAUSE: php_call_types (lang_specs.c) is
|
||
* {member_call_expression, scoped_call_expression, function_call_expression,
|
||
* nullsafe_member_call_expression} — it omits "object_creation_expression",
|
||
* so a PHP `new T()` is never extracted as a call. (php/S2 method-dispatch via
|
||
* `new Counter()` + $c->inc() passes because the METHOD calls resolve; only the
|
||
* constructor invocation itself is unmodeled.) */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S3/constructor", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S4 — PHP static method call (Class::method). */
|
||
TEST(lrp_php_s4_static_method) {
|
||
static const LRP_File f[] = {
|
||
{"Config.php",
|
||
"<?php\nclass Config {\n public static function version() { return '1.0'; }\n}\n"},
|
||
{"app.php", "<?php\nrequire_once 'Config.php';\n"
|
||
"function run() { return Config::version(); }\n"}};
|
||
/* GREEN: Config::version() — php_lsp_cross resolves static method call. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S4/static_method", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S5 — PHP chained method call. */
|
||
TEST(lrp_php_s5_chained) {
|
||
static const LRP_File f[] = {
|
||
{"Builder.php", "<?php\nclass Builder {\n private $items = [];\n"
|
||
" public function add($x) { $this->items[] = $x; return $this; }\n"
|
||
" public function build() { return $this->items; }\n}\n"},
|
||
{"main.php", "<?php\nrequire_once 'Builder.php';\n"
|
||
"function run() { return (new Builder())->add(1)->add(2)->build(); }\n"}};
|
||
/* GREEN: (new Builder())->add()->add()->build() — php_lsp_cross tracks
|
||
* self-returning methods (return $this). */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S5/chained", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S6 — PHP inherited method call. */
|
||
TEST(lrp_php_s6_inherited_method) {
|
||
static const LRP_File f[] = {
|
||
{"Base.php", "<?php\nclass Base {\n public function describe() { return 'base'; }\n}\n"},
|
||
{"Child.php", "<?php\nrequire_once 'Base.php';\nclass Child extends Base {\n"
|
||
" public function run() { return $this->describe(); }\n}\n"}};
|
||
/* Uncertain: $this->describe() in Child::run() — php_lsp_cross must see
|
||
* Child extends Base. PHP extraction bug (base_classes not populated)
|
||
* may block this end-to-end. Assert CALLS >= 1. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S6/inherited_method", 0));
|
||
PASS();
|
||
}
|
||
|
||
/* S7 — PHP interface method call (type-hinted parameter). */
|
||
TEST(lrp_php_s7_interface_call) {
|
||
static const LRP_File f[] = {
|
||
{"LoggerInterface.php",
|
||
"<?php\ninterface LoggerInterface {\n public function log($msg);\n}\n"},
|
||
{"FileLogger.php",
|
||
"<?php\nrequire_once 'LoggerInterface.php';\n"
|
||
"class FileLogger implements LoggerInterface {\n"
|
||
" public function log($msg) { file_put_contents('/tmp/log', $msg); }\n}\n"},
|
||
{"Service.php", "<?php\nrequire_once 'LoggerInterface.php';\n"
|
||
"class Service {\n private $logger;\n"
|
||
" public function __construct(LoggerInterface $logger) {\n"
|
||
" $this->logger = $logger;\n }\n"
|
||
" public function run($msg) { $this->logger->log($msg); }\n}\n"}};
|
||
/* GREEN: $this->logger is type-hinted as LoggerInterface; php_lsp_cross
|
||
* resolves $this->logger->log() to LoggerInterface::log. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 3, 1, "php/S7/interface_call", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* S8 — PHP field-type-hint (property with docblock type). */
|
||
TEST(lrp_php_s8_field_type_hint) {
|
||
static const LRP_File f[] = {
|
||
{"Mailer.php",
|
||
"<?php\nclass Mailer {\n public function send($to, $body) { return true; }\n}\n"},
|
||
{"Notifier.php", "<?php\nrequire_once 'Mailer.php';\n"
|
||
"class Notifier {\n /** @var Mailer */\n private $mailer;\n"
|
||
" public function __construct(Mailer $mailer) {\n"
|
||
" $this->mailer = $mailer;\n }\n"
|
||
" public function notify($to, $msg) {\n"
|
||
" $this->mailer->send($to, $msg);\n }\n}\n"}};
|
||
/* GREEN: $this->mailer is type-hinted as Mailer in constructor parameter;
|
||
* php_lsp_cross resolves $this->mailer->send() to Mailer::send. */
|
||
ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S8/field_type_hint", 1));
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* ─── BONUS: USAGE-EDGE PROBES (type references, not calls) ───────────
|
||
* ══════════════════════════════════════════════════════════════════════
|
||
* USAGE edges are created for non-call type references. Constructor syntax is
|
||
* a CALLS relationship; typed-parameter scenarios remain USAGE.
|
||
*/
|
||
|
||
/* Go: struct literal usage of a cross-file type. */
|
||
TEST(lrp_go_usage_struct_literal) {
|
||
static const LRP_File f[] = {
|
||
{"types.go", "package app\n\ntype Config struct{ Port int }\n"},
|
||
{"main.go", "package app\n\nfunc Make() Config { return Config{Port: 8080} }\n"}};
|
||
LRP_Proj lp;
|
||
int n = lrp_usage(&lp, f, 2);
|
||
/* GREEN: USAGE edge from Make to Config type (struct literal). */
|
||
if (n < 1) {
|
||
fprintf(stderr, " [LRP] go/USAGE/struct_literal n=%d expected>=1\n", n);
|
||
}
|
||
ASSERT_TRUE(n >= 1);
|
||
PASS();
|
||
}
|
||
|
||
/* TypeScript: exact imported type reference as parameter annotation. */
|
||
TEST(lrp_ts_usage_type_param) {
|
||
static const LRP_File f[] = {
|
||
{"types.ts", "export interface Config { timeout: number; }\n"},
|
||
{"main.ts", "import { Config } from './types';\n\n"
|
||
"export function run(cfg: Config): number { return cfg.timeout; }\n"}};
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||
int exact = store ? lrp_exact_edge_by_name(store, lp.project, "USAGE", "run", "Config") : -1;
|
||
lrp_cleanup(&lp, store);
|
||
ASSERT_EQ(exact, 1);
|
||
PASS();
|
||
}
|
||
|
||
static int lrp_ts_imported_type_with_decoy(const char *main_filename) {
|
||
LRP_File files[] = {
|
||
{"a/types.ts", "export interface Config { selected: number; }\n"},
|
||
{"b/types.ts", "export interface Config { decoy: string; }\n"},
|
||
{main_filename, "import type { Config } from './a/types';\n"
|
||
"export function choose(cfg: Config): number { return cfg.selected; }\n"},
|
||
};
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, files, 3);
|
||
int exact = store ? lrp_exact_edge_by_qn_suffix(store, lp.project, "USAGE", "main.choose",
|
||
"a.types.Config")
|
||
: -1;
|
||
int decoy = store ? lrp_exact_edge_by_qn_suffix(store, lp.project, "USAGE", "main.choose",
|
||
"b.types.Config")
|
||
: -1;
|
||
lrp_cleanup(&lp, store);
|
||
return exact == 1 && decoy == 0;
|
||
}
|
||
|
||
/* A duplicate type name must not let unique-name fallback hide import identity. */
|
||
TEST(lrp_ts_usage_import_type_with_decoy) {
|
||
ASSERT_TRUE(lrp_ts_imported_type_with_decoy("main.ts"));
|
||
PASS();
|
||
}
|
||
|
||
TEST(lrp_tsx_usage_import_type_with_decoy) {
|
||
ASSERT_TRUE(lrp_ts_imported_type_with_decoy("main.tsx"));
|
||
PASS();
|
||
}
|
||
|
||
/* Python: constructor syntax is an exact call to the materialized Class node. */
|
||
TEST(lrp_python_usage_instantiation) {
|
||
static const LRP_File f[] = {
|
||
{"model.py", "class User:\n def __init__(self, name):\n self.name = name\n"},
|
||
{"main.py", "from .model import User\n\n\ndef create(name):\n return User(name)\n"}};
|
||
LRP_Proj lp;
|
||
cbm_store_t *store = lrp_index(&lp, f, 2);
|
||
int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1;
|
||
lrp_cleanup(&lp, store);
|
||
ASSERT_EQ(exact, 1);
|
||
PASS();
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════
|
||
* SUITE registration
|
||
* ══════════════════════════════════════════════════════════════════════ */
|
||
|
||
SUITE(lsp_resolution_probe) {
|
||
|
||
/* ── Go (lsp_cross WIRED) — all 8 scenarios GREEN ── */
|
||
RUN_TEST(lrp_go_s1_crossfile_call);
|
||
RUN_TEST(lrp_go_s2_method_dispatch);
|
||
RUN_TEST(lrp_go_s3_constructor);
|
||
RUN_TEST(lrp_go_s4_static_call);
|
||
RUN_TEST(lrp_go_s5_chained_call);
|
||
RUN_TEST(lrp_go_s6_inherited_method);
|
||
RUN_TEST(lrp_go_s7_generic_call);
|
||
RUN_TEST(lrp_go_s8_field_type_hint);
|
||
|
||
/* ── C (lsp_cross WIRED) — S1/S3/S5–S8 GREEN, S2/S4/S7 uncertain ── */
|
||
RUN_TEST(lrp_c_s1_crossfile_call);
|
||
RUN_TEST(lrp_c_s2_funcptr_in_struct);
|
||
RUN_TEST(lrp_c_s3_constructor);
|
||
RUN_TEST(lrp_c_s4_static_local);
|
||
RUN_TEST(lrp_c_s5_chained_deref);
|
||
RUN_TEST(lrp_c_s6_base_method);
|
||
RUN_TEST(lrp_c_s7_generic_callback);
|
||
RUN_TEST(lrp_c_s8_field_call);
|
||
|
||
/* ── C++ (lsp_cross WIRED) — GREEN expected; virtual/template uncertain ── */
|
||
RUN_TEST(lrp_cpp_s1_crossfile_call);
|
||
RUN_TEST(lrp_cpp_s2_method_dispatch);
|
||
RUN_TEST(lrp_cpp_s3_constructor);
|
||
RUN_TEST(lrp_cpp_s4_static_method);
|
||
RUN_TEST(lrp_cpp_s5_chained);
|
||
RUN_TEST(lrp_cpp_s6_virtual_inherited);
|
||
RUN_TEST(lrp_cpp_s7_template);
|
||
RUN_TEST(lrp_cpp_s8_field_call);
|
||
|
||
/* ── Rust (lsp_cross NOT WIRED) — S1 GREEN, S2–S8 RED reproductions ── */
|
||
RUN_TEST(lrp_rust_s1_crossfile_call);
|
||
RUN_TEST(lrp_rust_s2_method_dispatch);
|
||
RUN_TEST(lrp_rust_s3_constructor);
|
||
RUN_TEST(lrp_rust_s4_static_method);
|
||
RUN_TEST(lrp_rust_s5_chained);
|
||
RUN_TEST(lrp_rust_s6_trait_method);
|
||
RUN_TEST(lrp_rust_s7_generic);
|
||
RUN_TEST(lrp_rust_s8_field_call);
|
||
RUN_TEST(lrp_rust_crossfile_macro_hidden_call_carrier);
|
||
|
||
/* ── Python (lsp_cross WIRED) — S1–S5,S7,S8 GREEN; S6 suppressed by the
|
||
* weak-member guard (#1276), asserted == 0 until py_lsp_cross inherits ── */
|
||
RUN_TEST(lrp_python_s1_crossfile_call);
|
||
RUN_TEST(lrp_python_s2_method_dispatch);
|
||
RUN_TEST(lrp_python_s3_constructor);
|
||
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);
|
||
|
||
/* ── TypeScript (lsp_cross WIRED) — S1–S5,S7,S8 GREEN; S6 uncertain ── */
|
||
RUN_TEST(lrp_ts_s1_crossfile_call);
|
||
RUN_TEST(lrp_ts_s2_method_dispatch);
|
||
RUN_TEST(lrp_ts_s3_constructor);
|
||
RUN_TEST(lrp_ts_s4_static_method);
|
||
RUN_TEST(lrp_ts_s5_chained);
|
||
RUN_TEST(lrp_ts_s6_inherited_method);
|
||
RUN_TEST(lrp_ts_s7_generic);
|
||
RUN_TEST(lrp_ts_s8_field_type_hint);
|
||
|
||
/* ── Java (lsp_cross NOT WIRED) — S1 GREEN, S2–S8 RED reproductions ── */
|
||
RUN_TEST(lrp_java_s1_crossfile_call);
|
||
RUN_TEST(lrp_java_s2_method_dispatch);
|
||
RUN_TEST(lrp_java_s3_constructor);
|
||
RUN_TEST(lrp_java_s4_static_method);
|
||
RUN_TEST(lrp_java_s5_chained);
|
||
RUN_TEST(lrp_java_s6_inherited_method);
|
||
RUN_TEST(lrp_java_s7_generic);
|
||
RUN_TEST(lrp_java_s8_field_type_hint);
|
||
|
||
/* ── Kotlin (no cross-file LSP) — S1 GREEN, S2–S8 RED reproductions ── */
|
||
RUN_TEST(lrp_kotlin_s1_crossfile_call);
|
||
RUN_TEST(lrp_kotlin_s2_method_dispatch);
|
||
RUN_TEST(lrp_kotlin_s3_constructor);
|
||
RUN_TEST(lrp_kotlin_s4_companion_static);
|
||
RUN_TEST(lrp_kotlin_s5_chained);
|
||
RUN_TEST(lrp_kotlin_s6_inherited_method);
|
||
RUN_TEST(lrp_kotlin_s7_generic);
|
||
RUN_TEST(lrp_kotlin_s8_field_type_hint);
|
||
|
||
/* ── C# (lsp_cross NOT WIRED — but cbm_run_cs_lsp_cross EXISTS) ── */
|
||
/* S1 GREEN, S2–S8 RED reproductions */
|
||
RUN_TEST(lrp_csharp_s1_crossfile_call);
|
||
RUN_TEST(lrp_csharp_s2_method_dispatch);
|
||
RUN_TEST(lrp_csharp_s3_constructor);
|
||
RUN_TEST(lrp_csharp_s4_static_method);
|
||
RUN_TEST(lrp_csharp_s5_chained);
|
||
RUN_TEST(lrp_csharp_s6_inherited_method);
|
||
RUN_TEST(lrp_csharp_s7_generic);
|
||
RUN_TEST(lrp_csharp_s8_field_type_hint);
|
||
|
||
/* ── PHP (lsp_cross WIRED) — S1–S5 GREEN; S6 uncertain; S7,S8 GREEN ── */
|
||
RUN_TEST(lrp_php_s1_crossfile_call);
|
||
RUN_TEST(lrp_php_s2_method_dispatch);
|
||
RUN_TEST(lrp_php_s3_constructor);
|
||
RUN_TEST(lrp_php_s4_static_method);
|
||
RUN_TEST(lrp_php_s5_chained);
|
||
RUN_TEST(lrp_php_s6_inherited_method);
|
||
RUN_TEST(lrp_php_s7_interface_call);
|
||
RUN_TEST(lrp_php_s8_field_type_hint);
|
||
|
||
/* ── USAGE-edge bonus probes ── */
|
||
RUN_TEST(lrp_go_usage_struct_literal);
|
||
RUN_TEST(lrp_ts_usage_type_param);
|
||
RUN_TEST(lrp_ts_usage_import_type_with_decoy);
|
||
RUN_TEST(lrp_tsx_usage_import_type_with_decoy);
|
||
RUN_TEST(lrp_python_usage_instantiation);
|
||
}
|