Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

269 lines
13 KiB
C

/*
* repro_issue546.c — Reproduce-first case for OPEN bug #546.
*
* Issue: #546 — "trace_path / reverse-dependency returns an INCOMPLETE caller
* set when a symbol is duplicated by an ambient .d.ts declaration
* (callers silently split by import style)"
*
* Root cause (graph layer — node identity / dedup across the ambient declaration):
* When a TypeScript symbol is BOTH defined in a real .ts source file AND
* re-declared (body-less, signature only) in an ambient .d.ts shim file,
* the indexer creates TWO distinct Function nodes for the same logical symbol
* (one rooted at the .ts implementation, one rooted at the .d.ts stub).
*
* CALLS edges from consumers are then partitioned across the two nodes based
* on which import form each consumer used:
* - consumer importing via relative path ("./scroll") → CALLS edge targets
* the IMPLEMENTATION node (packages/widget/src/scroll.ts)
* - consumer importing via path alias ("@widget") → CALLS edge targets
* the .d.ts STUB node (app/types/widget-shim.d.ts)
*
* trace_path resolves the symbol name to EXACTLY ONE of the two nodes (the
* first one returned by cbm_store_find_nodes_by_name) and BFS-traverses only
* that node's inbound CALLS edges. The callers whose edges point to the OTHER
* node are silently omitted from the result. There is no warning that the
* symbol resolved to multiple nodes and the caller set is therefore partial.
*
* Expected (correct) behaviour:
* trace_path(function_name="alignToEdge", direction="inbound") must return
* ALL callers, regardless of which import style they used:
* {"callers": [{name: "internalConsumer", ...}, {name: "externalConsumer", ...}]}
* Both "internalConsumer" AND "externalConsumer" must appear in the response.
*
* Actual (buggy) behaviour:
* Only ONE of the two callers appears in the "callers" array. The other is
* silently dropped because its CALLS edge points to the sibling node (the
* other representation of the same logical symbol) that trace_path did not
* select as its BFS root.
*
* Why RED on current code:
* The final assertion checks that BOTH caller names appear in the trace_path
* JSON response. On buggy code, trace_path picks one of the two Function
* nodes for "alignToEdge" as its BFS root; the inbound CALLS edges of the
* OTHER node are never visited; one caller name is absent from the JSON;
* the strstr check for the missing name returns NULL →
* ASSERT_NOT_NULL(strstr(resp, "...")) FAILS → RED.
*
* Precondition strategy:
* Before driving trace_path, the test checks that BOTH callers produced
* at least one CALLS edge each (total CALLS edges ≥ 2). If this precondition
* fires RED it flags an extraction failure (TS CALLS extraction not working),
* not the #546 traversal bug. Separation keeps the root cause unambiguous.
*
* TS CALLS extraction reliability note:
* TypeScript CALLS extraction is confirmed reliable for simple intra-package
* call expressions by existing integration tests (test_extraction.c and the
* regression suite). The known risk here is the path-alias import form
* ("@widget") — the extractor may or may not resolve the alias and produce
* a CALLS edge for externalConsumer. If the precondition (total CALLS ≥ 2)
* fires first, the alias resolution is the cause, not the #546 split.
* A secondary precondition after the main assertion ensures that even if only
* one CALLS edge is produced (alias unresolved), the test is still RED for
* the right reason: incomplete caller set.
*
* Fix location (not implemented here):
* Either in cbm_store_find_nodes_by_name / cbm_store_bfs (union traversal
* across all nodes sharing name+signature), or in the pipeline dedup step
* where body-less .d.ts stub nodes should be merged/aliased into their
* implementation counterpart rather than stored as separate graph nodes.
*/
#include <foundation/compat.h>
#include "test_framework.h"
#include "repro_harness.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* ── Fixture ────────────────────────────────────────────────────────────────
*
* Minimal TypeScript monorepo layout that triggers the dual-node split:
*
* packages/widget/src/scroll.ts
* — real implementation of alignToEdge(); exports the function
*
* packages/widget/src/internalConsumer.ts
* — imports alignToEdge via RELATIVE path ("./scroll")
* — calls alignToEdge(document.createElement('div'))
* → CALLS edge targets the IMPLEMENTATION node
*
* app/types/widget-shim.d.ts
* — ambient .d.ts declaration; body-less signature of alignToEdge
* — this causes the indexer to create a SECOND (stub) Function node
*
* app/src/externalConsumer.ts
* — imports alignToEdge via PATH ALIAS ("@widget")
* — calls alignToEdge(document.querySelector('div'))
* → CALLS edge targets the .d.ts STUB node (the alias points there)
*
* On buggy code: two Function nodes for "alignToEdge"; trace_path picks one;
* only one caller is returned.
*
* Note: The tsconfig.json is included so the indexer can, in principle,
* resolve the "@widget" path alias to packages/widget/src. Alias resolution
* is best-effort in the current extractor; even without it, if the .d.ts stub
* causes a second node, the externalConsumer CALLS edge will point to that
* stub node, and the test assertion will correctly turn RED.
*/
static const RFile k_files[] = {
/* tsconfig: maps @widget alias to packages/widget/src */
{
"tsconfig.json",
"{\n"
" \"compilerOptions\": {\n"
" \"baseUrl\": \".\",\n"
" \"paths\": {\n"
" \"@widget\": [\"packages/widget/src\"]\n"
" }\n"
" }\n"
"}\n"
},
/* Real implementation — produces the IMPLEMENTATION Function node */
{
"packages/widget/src/scroll.ts",
"export function alignToEdge(el: HTMLElement): () => void {\n"
" return function() { el.scrollIntoView({ block: 'nearest' }); };\n"
"}\n"
},
/* Internal consumer: relative import → CALLS edge → IMPLEMENTATION node */
{
"packages/widget/src/internalConsumer.ts",
"import { alignToEdge } from './scroll';\n"
"const node = document.createElement('div');\n"
"const cleanup = alignToEdge(node);\n"
"export { cleanup };\n"
},
/* Ambient .d.ts shim — triggers the SECOND (stub) Function node creation */
{
"app/types/widget-shim.d.ts",
"export function alignToEdge(el: HTMLElement): () => void;\n"
},
/* External consumer: alias import → CALLS edge → .d.ts STUB node */
{
"app/src/externalConsumer.ts",
"import { alignToEdge } from '@widget';\n"
"const div = document.querySelector('div') as HTMLElement;\n"
"const teardown = alignToEdge(div);\n"
"export { teardown };\n"
}
};
/* ─────────────────────────────────────────────────────────────────────────
* repro_issue546_dts_split_caller_set
*
* Precondition A (must be GREEN to prove extraction is working):
* At least 1 CALLS edge exists in the graph (the internalConsumer relative
* import is the most reliable and must produce a CALLS edge).
*
* The failing assertion (RED on buggy code):
* trace_path for "alignToEdge" with direction="inbound" returns a "callers"
* array that contains BOTH "internalConsumer" AND "externalConsumer".
*
* The test is RED when EITHER name is absent — the partial set is the bug.
* ───────────────────────────────────────────────────────────────────────── */
TEST(repro_issue546_dts_split_caller_set) {
RProj lp;
cbm_store_t *store = rh_index_files(&lp, k_files,
(int)(sizeof(k_files) / sizeof(k_files[0])));
ASSERT_NOT_NULL(store);
/* ── Precondition A: at least one CALLS edge must exist ─────────────
* If this fires RED, TS CALLS extraction is broken for this fixture —
* that is a pre-existing extraction bug, not #546. The test cannot
* distinguish the traversal split without any edges to split across.
*
* Minimum: 1 (internalConsumer's relative-path import always resolves).
* Ideally 2 (externalConsumer's alias import also resolves), but even
* 1 is enough to trigger the .d.ts node creation that causes the split.
*/
int calls_count = rh_count_edges(store, lp.project, "CALLS");
ASSERT_GT(calls_count, 0); /* precondition — not the #546 assertion */
/* ── Drive trace_path: inbound callers of "alignToEdge" ─────────────
*
* Args:
* function_name — bare symbol name; the indexer mints node names
* matching the short function name for both the impl
* and the .d.ts stub node.
* project — lp.project (derived from tmpdir)
* direction — "inbound": who calls alignToEdge?
* depth — 2: one hop is enough (caller → alignToEdge)
*
* On CORRECT code (fixed):
* trace_path unions all Function nodes named "alignToEdge" and returns
* callers from all of them:
* {"callers":[{"name":"internalConsumer",...},{"name":"externalConsumer",...}]}
*
* On BUGGY code (current):
* trace_path resolves "alignToEdge" to ONE node (first match from
* cbm_store_find_nodes_by_name). Only callers whose CALLS edges
* point to THAT node appear. The other caller is silently absent.
*/
char args[512];
snprintf(args, sizeof(args),
"{\"function_name\":\"alignToEdge\","
"\"project\":\"%s\","
"\"direction\":\"inbound\","
"\"depth\":2}",
lp.project);
char *resp = cbm_mcp_handle_tool(lp.srv, "trace_path", args);
ASSERT_NOT_NULL(resp);
/* Symbol must be found — if "function not found" fires, the name lookup
* itself has a problem unrelated to #546. */
ASSERT_NULL(strstr(resp, "function not found"));
/* "callers" key must appear (always emitted when direction is inbound).
* The response is the MCP envelope (inner json embedded as an escaped
* string), so the key appears as \"callers\" — match the escaped form. */
ASSERT_NOT_NULL(strstr(resp, "\\\"callers\\\""));
/* The callers array must not be empty — at least the internalConsumer
* (whose relative-path import is reliably resolved) must appear.
*
* WHY this might already be RED for #546:
* If trace_path selected the .d.ts stub node as BFS root, only
* externalConsumer is there; internalConsumer's edge is on the impl
* node, so this check fires RED immediately (callers:[]) or wrong name.
*/
ASSERT_NULL(strstr(resp, "\\\"callers\\\":[]")); /* empty = traversal totally wrong */
/* ── PRIMARY ASSERTION: BOTH callers must appear in the response ─────
*
* "internalConsumer" — imports via relative path, CALLS edge → impl node
* "externalConsumer" — imports via alias, CALLS edge → .d.ts stub node
*
* On CORRECT (fixed) code: trace_path unions both nodes; both names present.
*
* WHY RED on buggy code:
* trace_path selects ONE of the two "alignToEdge" nodes as its BFS root.
* Only that node's inbound CALLS edges are traversed. The caller whose
* CALLS edge points to the OTHER node is absent from the JSON response.
* strstr() for the missing caller name returns NULL, and ASSERT_NOT_NULL
* fires → RED.
*
* Concretely:
* — if impl node selected: "externalConsumer" absent → RED
* — if .d.ts node selected: "internalConsumer" absent → RED
* Either way, exactly one of the two assertions below is RED,
* proving the caller set is split and incomplete.
*/
ASSERT_NOT_NULL(strstr(resp, "internalConsumer")); /* relative-import caller */
ASSERT_NOT_NULL(strstr(resp, "externalConsumer")); /* alias-import caller */
free(resp);
rh_cleanup(&lp, store);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────────────── */
SUITE(repro_issue546) {
RUN_TEST(repro_issue546_dts_split_caller_set);
}