Skip to content

Commit c737413

Browse files
authored
Merge pull request DeusData#847 from DeusData/distill/ui-label-and-ts-repro
fix(graph-ui): honest Project-ID label; RED repro for TS inherited-method gap
2 parents 2e8f0f0 + b58d1d8 commit c737413

6 files changed

Lines changed: 275 additions & 5 deletions

File tree

Makefile.cbm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ TEST_REPRO_SRCS = \
483483
tests/repro/repro_lsp_c_cpp.c \
484484
tests/repro/repro_lsp_go_py.c \
485485
tests/repro/repro_lsp_ts.c \
486+
tests/repro/repro_ts_inherited_method.c \
486487
tests/repro/repro_lsp_java_cs.c \
487488
tests/repro/repro_lsp_kt_php_rust.c
488489

graph-ui/src/components/StatsTab.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe("StatsTab index modal", () => {
6666
fireEvent.change(await screen.findByLabelText("Repository path"), {
6767
target: { value: "D:\\work\\信租风控通后端" },
6868
});
69-
fireEvent.change(screen.getByLabelText("Project name"), {
69+
fireEvent.change(screen.getByLabelText("Project ID (optional — permanent, cannot be renamed)"), {
7070
target: { value: "信租风控通后端" },
7171
});
7272
fireEvent.click(screen.getByRole("button", { name: "Index This Folder" }));

graph-ui/src/components/StatsTab.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat
315315
onChange={(e) => setProjectName(e.target.value)}
316316
className="w-full bg-white/[0.04] border border-white/[0.06] rounded-lg px-3 py-2 text-[12px] text-foreground outline-none focus:border-primary/40 placeholder:text-foreground/20"
317317
/>
318+
<span className="block text-[10px] text-foreground/25 mt-1">{t.index.projectNameHelp}</span>
318319
</label>
319320
</div>
320321

graph-ui/src/lib/i18n.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ export const messages = {
4545
selectRepositoryFolder: "Select Repository Folder",
4646
instructions: "Navigate to the project root and click \"Index This Folder\".",
4747
repositoryPath: "Repository path",
48-
projectName: "Project name",
49-
projectNamePlaceholder: "Optional display name",
48+
projectName: "Project ID (optional — permanent, cannot be renamed)",
49+
projectNamePlaceholder: "Derived from folder name if blank",
50+
projectNameHelp: "Becomes the database name and query prefix. Leave blank to derive it from the path.",
5051
filterFolders: "Filter folders",
5152
noSubdirectories: "No subdirectories",
5253
indexThisFolder: "Index This Folder",
@@ -116,8 +117,9 @@ export const messages = {
116117
selectRepositoryFolder: "选择仓库目录",
117118
instructions: "导航到项目根目录,然后点击“索引此目录”。",
118119
repositoryPath: "仓库路径",
119-
projectName: "项目名称",
120-
projectNamePlaceholder: "可选显示名称",
120+
projectName: "项目 ID(可选,永久且不可重命名)",
121+
projectNamePlaceholder: "留空则从路径派生",
122+
projectNameHelp: "将作为数据库名称与查询前缀;留空则从路径派生。",
121123
filterFolders: "筛选目录",
122124
noSubdirectories: "没有子目录",
123125
indexThisFolder: "索引此目录",

tests/repro/repro_main.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ extern void suite_repro_grammar_misc(void);
106106
extern void suite_repro_lsp_c_cpp(void);
107107
extern void suite_repro_lsp_go_py(void);
108108
extern void suite_repro_lsp_ts(void);
109+
/* TS cross-file inherited-method resolution gap (post-#840 probe flip) */
110+
extern void suite_repro_ts_inherited_method(void);
109111
extern void suite_repro_lsp_java_cs(void);
110112
extern void suite_repro_lsp_kt_php_rust(void);
111113

@@ -174,6 +176,7 @@ int main(void) {
174176
RUN_SUITE(repro_lsp_c_cpp);
175177
RUN_SUITE(repro_lsp_go_py);
176178
RUN_SUITE(repro_lsp_ts);
179+
RUN_SUITE(repro_ts_inherited_method);
177180
RUN_SUITE(repro_lsp_java_cs);
178181
RUN_SUITE(repro_lsp_kt_php_rust);
179182

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* repro_ts_inherited_method.c — RED reproduction: TypeScript CROSS-FILE
3+
* INHERITED method call resolution gap (ts_lsp).
4+
*
5+
* THE GAP: a typed call to a method the receiver's class INHERITS from a base
6+
* class defined in ANOTHER file is never resolved by the TS LSP:
7+
*
8+
* base.ts: export class Base { greet(): string { ... } }
9+
* derived.ts: import { Base } from "./base";
10+
* export class Derived extends Base { ... }
11+
* export function callSite(): string {
12+
* const d: Derived = new Derived();
13+
* return d.greet(); <-- inherited, cross-file
14+
* }
15+
*
16+
* CORRECT behaviour (asserted here, so this test is RED until fixed): a CALLS
17+
* edge from the caller (callable-sourced, QN contains "callSite") to the BASE
18+
* method definition (target QN suffix ".Base.greet" — mirroring the
19+
* java/kotlin/php inherited-dispatch convention: resolution lands on the base
20+
* class's method def, since Derived declares no `greet` and no such node can
21+
* exist), carrying a genuine TS-LSP resolution strategy ("lsp_ts_*" in the
22+
* edge's properties_json, per the ts_emit_resolved_call contract documented in
23+
* repro_lsp_ts.c).
24+
*
25+
* WHY the strategy tag is part of the invariant: before the weak-short-name
26+
* suppression (PR #840, recovering withdrawn #836/#835), this scenario looked
27+
* resolved via a unique_name REGISTRY fallback — "greet" is unique in a 2-file
28+
* fixture, so a short-name guess happened to bind the right node (a false
29+
* green; in a real repo the same guess binds an arbitrary same-named method).
30+
* PR #840 flipped the probe lrp_ts_s6_inherited_method
31+
* (tests/test_lsp_resolution_probe.c) to assert that this weak edge is
32+
* SUPPRESSED (calls == 0) — correct, but it leaves the underlying resolution
33+
* gap without any red reproduction. THIS test is that reproduction:
34+
* - on pre-#840 code the lucky edge exists but carries NO "lsp_ts" strategy
35+
* -> RED (the green was never genuine);
36+
* - on post-#840 code the weak edge is suppressed, no edge exists at all
37+
* -> RED;
38+
* - only genuine ts_lsp cross-file inheritance resolution turns it GREEN.
39+
*
40+
* ROOT-CAUSE POINTER (for the eventual fixer): ts_lsp cross-file inheritance
41+
* resolution — internal/cbm/lsp/ts_lsp.c resolve_member_call/lookup_method
42+
* only walks methods declared on the receiver's OWN class as registered in the
43+
* module registry; it does not follow the (correctly extracted) INHERITS edge
44+
* from Derived to an imported Base to find `greet` there. See PR #836/#840 and
45+
* the S6 probe lrp_ts_s6_inherited_method for the full analysis. The INHERITS
46+
* edge and both defs ARE in the graph (asserted below as preconditions), so a
47+
* red here is the RESOLUTION gap, not an extraction failure.
48+
*
49+
* NOTE: line comments only inside this header (no nested block comments, per
50+
* coding rules).
51+
*/
52+
53+
#include "test_framework.h"
54+
#include "repro_invariant_lib.h"
55+
#include <store/store.h>
56+
57+
#include <string.h>
58+
59+
/* ── Fixture ─────────────────────────────────────────────────────────────── */
60+
61+
static const RFile kTsInherited[] = {
62+
{"base.ts",
63+
"export class Base {\n"
64+
" greet(): string { return \"hello\"; }\n"
65+
"}\n"},
66+
{"derived.ts",
67+
"import { Base } from \"./base\";\n"
68+
"\n"
69+
"export class Derived extends Base {\n"
70+
" extra(): number { return 2; }\n"
71+
"}\n"
72+
"\n"
73+
"export function callSite(): string {\n"
74+
" const d: Derived = new Derived();\n"
75+
" return d.greet();\n"
76+
"}\n"},
77+
};
78+
79+
/* ── Local store helpers ─────────────────────────────────────────────────── */
80+
81+
/* True if some node with `label` has a QN ending in `suffix`. */
82+
static int node_with_qn_suffix(cbm_store_t *store, const char *project,
83+
const char *label, const char *suffix) {
84+
cbm_node_t *nodes = NULL;
85+
int count = 0;
86+
if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) !=
87+
CBM_STORE_OK)
88+
return 0;
89+
int found = 0;
90+
size_t sl = strlen(suffix);
91+
for (int i = 0; i < count && !found; i++) {
92+
const char *qn = nodes[i].qualified_name;
93+
if (qn) {
94+
size_t ql = strlen(qn);
95+
if (ql >= sl && strcmp(qn + ql - sl, suffix) == 0)
96+
found = 1;
97+
}
98+
}
99+
cbm_store_free_nodes(nodes, count);
100+
return found;
101+
}
102+
103+
/*
104+
* The PRIMARY invariant, checked on a SINGLE edge (independent per-edge checks
105+
* could false-green by combining a strategy-less lucky edge with an unrelated
106+
* lsp_ts-tagged edge): there exists a CALLS edge whose
107+
* - source is callable-sourced (Function/Method) and its QN contains
108+
* `caller_substr`;
109+
* - target QN ends with `callee_suffix`;
110+
* - properties_json carries `strategy_substr` (substring, so any concrete
111+
* "lsp_ts_*" tag satisfies "lsp_ts").
112+
* When `dump` is non-zero every CALLS edge is printed to stderr so a RED run
113+
* documents exactly what the graph contains instead.
114+
*/
115+
static int lsp_resolved_edge_exists(cbm_store_t *store, const char *project,
116+
const char *caller_substr,
117+
const char *callee_suffix,
118+
const char *strategy_substr, int dump) {
119+
cbm_edge_t *edges = NULL;
120+
int n = 0;
121+
if (cbm_store_find_edges_by_type(store, project, "CALLS", &edges, &n) !=
122+
CBM_STORE_OK)
123+
return 0;
124+
int found = 0;
125+
size_t cl = strlen(callee_suffix);
126+
for (int i = 0; i < n; i++) {
127+
cbm_node_t src, tgt;
128+
if (cbm_store_find_node_by_id(store, edges[i].source_id, &src) != CBM_STORE_OK)
129+
continue;
130+
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
131+
continue;
132+
if (dump)
133+
fprintf(stderr, " [ts-inherited] CALLS %s (%s) -> %s props=%s\n",
134+
src.qualified_name ? src.qualified_name : "?",
135+
src.label ? src.label : "?",
136+
tgt.qualified_name ? tgt.qualified_name : "?",
137+
edges[i].properties_json ? edges[i].properties_json : "{}");
138+
const char *slabel = src.label ? src.label : "";
139+
if (strcmp(slabel, "Function") != 0 && strcmp(slabel, "Method") != 0)
140+
continue;
141+
if (!src.qualified_name || !strstr(src.qualified_name, caller_substr))
142+
continue;
143+
const char *tqn = tgt.qualified_name;
144+
if (!tqn)
145+
continue;
146+
size_t tl = strlen(tqn);
147+
if (tl < cl || strcmp(tqn + tl - cl, callee_suffix) != 0)
148+
continue;
149+
if (!edges[i].properties_json ||
150+
!strstr(edges[i].properties_json, strategy_substr))
151+
continue;
152+
found = 1;
153+
}
154+
cbm_store_free_edges(edges, n);
155+
return found;
156+
}
157+
158+
/* ── Tests ───────────────────────────────────────────────────────────────── */
159+
160+
/*
161+
* Extraction-tier preconditions — expected GREEN on current code. These prove
162+
* a red in the pipeline test below is the RESOLUTION gap, not a fixture or
163+
* extraction error: both files parse without has_error, base.ts yields the
164+
* Method def `greet`, and derived.ts yields the `greet` call site.
165+
*/
166+
TEST(repro_ts_inherited_extraction_preconditions) {
167+
/* base.ts extracts cleanly and defines Method greet. */
168+
ASSERT_TRUE(inv_extract_clean(kTsInherited[0].content, CBM_LANG_TYPESCRIPT,
169+
"base.ts"));
170+
CBMFileResult *rb =
171+
inv_rx(kTsInherited[0].content, CBM_LANG_TYPESCRIPT, "base.ts");
172+
ASSERT_NOT_NULL(rb);
173+
int greet_methods = 0;
174+
for (int i = 0; i < rb->defs.count; i++) {
175+
CBMDefinition *d = &rb->defs.items[i];
176+
if (d->label && strcmp(d->label, "Method") == 0 && d->name &&
177+
strcmp(d->name, "greet") == 0)
178+
greet_methods++;
179+
}
180+
cbm_free_result(rb);
181+
ASSERT_EQ(greet_methods, 1);
182+
183+
/* derived.ts extracts cleanly and contains the greet call site. */
184+
ASSERT_TRUE(inv_extract_clean(kTsInherited[1].content, CBM_LANG_TYPESCRIPT,
185+
"derived.ts"));
186+
CBMFileResult *rd =
187+
inv_rx(kTsInherited[1].content, CBM_LANG_TYPESCRIPT, "derived.ts");
188+
ASSERT_NOT_NULL(rd);
189+
int has_greet_call = inv_has_call(rd, "greet");
190+
cbm_free_result(rd);
191+
ASSERT_TRUE(has_greet_call);
192+
PASS();
193+
}
194+
195+
/*
196+
* THE RED REPRODUCTION — index the 2-file fixture through the full production
197+
* pipeline and assert the CORRECT outcome: an LSP-resolved CALLS edge from
198+
* callSite to Base.greet. Store-level preconditions (callee node present,
199+
* INHERITS extracted) are checked first so the failure is attributable to the
200+
* missing ts_lsp cross-file inheritance resolution and nothing else.
201+
* Returns 0 on PASS (gap fixed), non-zero on FAIL (RED = the open gap).
202+
*/
203+
static int run_ts_inherited_pipeline(void) {
204+
RProj lp;
205+
cbm_store_t *store = rh_index_files(
206+
&lp, kTsInherited, (int)(sizeof(kTsInherited) / sizeof(kTsInherited[0])));
207+
if (!store) {
208+
printf(" %sFAIL%s %s:%d: index failed (setup, NOT the gap)\n", tf_red(),
209+
tf_reset(), __FILE__, __LINE__);
210+
rh_cleanup(&lp, store);
211+
return 1;
212+
}
213+
214+
int rc = 0;
215+
216+
/* Precondition: the callee def node exists in the graph. */
217+
if (!node_with_qn_suffix(store, lp.project, "Method", ".Base.greet")) {
218+
printf(" %sFAIL%s %s:%d: precondition — no Method node with QN suffix "
219+
"\".Base.greet\" (extraction problem, NOT the resolution gap)\n",
220+
tf_red(), tf_reset(), __FILE__, __LINE__);
221+
rc = 1;
222+
}
223+
224+
/* Precondition: `Derived extends Base` produced an INHERITS edge (the S6
225+
* probe diagnostic confirms extraction emits it; the gap is downstream). */
226+
int inherits = rh_count_edges(store, lp.project, "INHERITS");
227+
if (inherits < 1) {
228+
printf(" %sFAIL%s %s:%d: precondition — INHERITS=%d (expected >=1; "
229+
"extraction problem, NOT the resolution gap)\n",
230+
tf_red(), tf_reset(), __FILE__, __LINE__, inherits);
231+
rc = 1;
232+
}
233+
234+
/* PRIMARY: the inherited call is resolved BY THE TS LSP — a CALLS edge
235+
* callSite -> *.Base.greet carrying an "lsp_ts" strategy. A short-name
236+
* registry fallback edge (no lsp_ts tag) does NOT satisfy this; nor does
237+
* post-#840 suppression (no edge at all). */
238+
if (!lsp_resolved_edge_exists(store, lp.project, "callSite", ".Base.greet",
239+
"lsp_ts", 0)) {
240+
/* Dump what the graph actually contains so the RED row documents it. */
241+
(void)lsp_resolved_edge_exists(store, lp.project, "callSite",
242+
".Base.greet", "lsp_ts", 1);
243+
printf(" %sFAIL%s %s:%d: no lsp_ts-resolved CALLS edge callSite -> "
244+
"*.Base.greet — TS cross-file INHERITED method call is "
245+
"UNRESOLVED (ts_lsp inheritance gap, see #836/#840)\n",
246+
tf_red(), tf_reset(), __FILE__, __LINE__);
247+
rc = 1;
248+
}
249+
250+
rh_cleanup(&lp, store);
251+
return rc;
252+
}
253+
254+
TEST(repro_ts_inherited_method_call_resolution) {
255+
return run_ts_inherited_pipeline();
256+
}
257+
258+
/* ── Suite ───────────────────────────────────────────────────────────────── */
259+
260+
SUITE(repro_ts_inherited_method) {
261+
RUN_TEST(repro_ts_inherited_extraction_preconditions);
262+
RUN_TEST(repro_ts_inherited_method_call_resolution);
263+
}

0 commit comments

Comments
 (0)