Skip to content

Commit fc2060b

Browse files
author
Your Name
committed
fix(csharp): Interface registration + polymorphic BFS bridging + this.Property WRITES
Three C# code intelligence fixes: 1. Register Interface nodes in the symbol registry. The registry filter only accepted Function/Method/Class labels. Interface nodes were never registered, so resolve_as_class('IExamService') always returned NULL. INHERITS edges from C# classes to their interfaces were never created (0 → Interface out of 1588 total INHERITS in the largest C# repo). Fix: add 'Interface' to the registration filter in both pass_definitions.c (sequential path) and pass_parallel.c (parallel path, cbm_build_registry_from_cache). Additionally, in pass_semantic.c, when the resolved base type is a Class but the name follows C# interface convention (I + uppercase), search for an Interface node with the same name and prefer it. This handles cross- language name collisions (e.g. C++ ILogger class vs C# ILogger interface). Result: CometExamService → IExamService INHERITS edge now created. 2. BFS interface method bridging in trace_call_path and get_impact. When tracing callers of a concrete method (e.g. PerformSessionAction), BFS now also checks if the method's enclosing class INHERITS from an Interface. If so, it finds the interface's method with the same name and runs additional BFS from that node. This surfaces callers that use the interface type (ISimCoordinator.PerformSessionAction) which previously showed 0 callers. Applied to both handle_trace_call_path (incoming.calls section) and handle_get_impact (multi-method BFS merge). 3. Expand WRITES detection for this.Property = value. handle_readwrites() only accepted bare identifiers as assignment LHS. C#/Java/TypeScript this.PropertyName = value has LHS as member_access_expression, which was skipped. Fix: add branch for member_access_expression / member_expression LHS. When the receiver is this/base/self, extract the property name and push as a write. Handles C# this_expression, Java this, TypeScript this, Python self. Expected: hundreds of additional WRITES edges from method bodies to class properties in C# repos. Requires full reindex of C# repos to rebuild INHERITS edges and WRITES.
1 parent 548bb3a commit fc2060b

File tree

5 files changed

+144
-4
lines changed

5 files changed

+144
-4
lines changed

internal/cbm/extract_semantic.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,38 @@ void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
275275
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
276276
}
277277
}
278+
/* member_access_expression: handle this.Property = value
279+
* for C#, Java, TypeScript, JavaScript.
280+
* Extracts the property name when the receiver is "this" or "base". */
281+
else if (strcmp(lk, "member_access_expression") == 0 ||
282+
strcmp(lk, "member_expression") == 0) {
283+
TSNode expr = ts_node_child_by_field_name(left, "expression", 10);
284+
TSNode name_node = ts_node_child_by_field_name(left, "name", 4);
285+
/* member_expression (JS/TS) uses "object" + "property" field names */
286+
if (ts_node_is_null(name_node)) {
287+
name_node = ts_node_child_by_field_name(left, "property", 8);
288+
}
289+
if (ts_node_is_null(expr)) {
290+
expr = ts_node_child_by_field_name(left, "object", 6);
291+
}
292+
if (!ts_node_is_null(name_node) && !ts_node_is_null(expr)) {
293+
const char *ek = ts_node_type(expr);
294+
/* Accept: this.X, base.X, self.X (Python) */
295+
if (strcmp(ek, "this_expression") == 0 ||
296+
strcmp(ek, "this") == 0 ||
297+
strcmp(ek, "base_expression") == 0 ||
298+
strcmp(ek, "self") == 0) {
299+
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
300+
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
301+
CBMReadWrite rw;
302+
rw.var_name = name;
303+
rw.is_write = true;
304+
rw.enclosing_func_qn = state->enclosing_func_qn;
305+
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
306+
}
307+
}
308+
}
309+
}
278310
}
279311
}
280312
}

src/mcp/mcp.c

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,57 @@ static char *handle_get_impact(cbm_mcp_server_t *srv, const char *args) {
16041604
free(subs);
16051605
}
16061606

1607+
/* Interface method bridging: if any start_id method implements an interface
1608+
* method, also BFS from the interface version to find polymorphic callers. */
1609+
{
1610+
struct sqlite3 *sdb = cbm_store_get_db(store);
1611+
for (int s = 0; s < start_id_count; s++) {
1612+
sqlite3_stmt *iface_stmt = NULL;
1613+
sqlite3_prepare_v2(sdb,
1614+
"SELECT im.id FROM edges e_dm "
1615+
"JOIN edges e_inh ON e_dm.source_id = e_inh.source_id AND e_inh.type = 'INHERITS' "
1616+
"JOIN nodes iface ON iface.id = e_inh.target_id AND iface.label = 'Interface' "
1617+
"JOIN edges e_im ON e_im.source_id = iface.id AND e_im.type = 'DEFINES_METHOD' "
1618+
"JOIN nodes im ON im.id = e_im.target_id "
1619+
"JOIN nodes m ON m.id = ?1 "
1620+
"WHERE e_dm.target_id = ?1 AND e_dm.type = 'DEFINES_METHOD' "
1621+
"AND im.name = m.name",
1622+
-1, &iface_stmt, NULL);
1623+
if (iface_stmt) {
1624+
sqlite3_bind_int64(iface_stmt, 1, start_ids[s]);
1625+
while (sqlite3_step(iface_stmt) == SQLITE_ROW) {
1626+
int64_t iface_method_id = sqlite3_column_int64(iface_stmt, 0);
1627+
cbm_traverse_result_t itr = {0};
1628+
cbm_store_bfs(store, iface_method_id, bfs_dir, call_types, 4,
1629+
max_depth, 50, &itr);
1630+
/* Merge interface callers into tr */
1631+
if (itr.visited_count > 0) {
1632+
int new_cap = tr.visited_count + itr.visited_count;
1633+
cbm_node_hop_t *new_v = realloc(tr.visited,
1634+
(size_t)new_cap * sizeof(cbm_node_hop_t));
1635+
if (new_v) {
1636+
tr.visited = new_v;
1637+
for (int iv = 0; iv < itr.visited_count; iv++) {
1638+
/* Dedup by node id */
1639+
bool dup = false;
1640+
for (int j = 0; j < tr.visited_count; j++) {
1641+
if (tr.visited[j].node.id == itr.visited[iv].node.id) {
1642+
dup = true; break;
1643+
}
1644+
}
1645+
if (!dup) {
1646+
tr.visited[tr.visited_count++] = itr.visited[iv];
1647+
}
1648+
}
1649+
}
1650+
}
1651+
free(itr.edges);
1652+
}
1653+
sqlite3_finalize(iface_stmt);
1654+
}
1655+
}
1656+
}
1657+
16071658
/* Group by depth */
16081659
yyjson_mut_val *d1_arr = yyjson_mut_arr(doc);
16091660
yyjson_mut_val *d2_arr = yyjson_mut_arr(doc);
@@ -2420,6 +2471,37 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
24202471
tr_count++;
24212472
}
24222473
}
2474+
2475+
/* Interface method bridging: if the target method implements an interface
2476+
* method, also find callers of the interface version. This handles C#/Java
2477+
* polymorphism where callers use `IFoo.method()` not `Foo.method()`. */
2478+
{
2479+
struct sqlite3 *sdb = cbm_store_get_db(store);
2480+
for (int s = 0; s < start_id_count && tr_count < MAX_TR; s++) {
2481+
/* Find: start_id's class → INHERITS → Interface → DEFINES_METHOD → same name */
2482+
sqlite3_stmt *iface_stmt = NULL;
2483+
sqlite3_prepare_v2(sdb,
2484+
"SELECT im.id FROM edges e_dm "
2485+
"JOIN edges e_inh ON e_dm.source_id = e_inh.source_id AND e_inh.type = 'INHERITS' "
2486+
"JOIN nodes iface ON iface.id = e_inh.target_id AND iface.label = 'Interface' "
2487+
"JOIN edges e_im ON e_im.source_id = iface.id AND e_im.type = 'DEFINES_METHOD' "
2488+
"JOIN nodes im ON im.id = e_im.target_id "
2489+
"JOIN nodes m ON m.id = ?1 "
2490+
"WHERE e_dm.target_id = ?1 AND e_dm.type = 'DEFINES_METHOD' "
2491+
"AND im.name = m.name",
2492+
-1, &iface_stmt, NULL);
2493+
if (iface_stmt) {
2494+
sqlite3_bind_int64(iface_stmt, 1, start_ids[s]);
2495+
while (sqlite3_step(iface_stmt) == SQLITE_ROW && tr_count < MAX_TR) {
2496+
int64_t iface_method_id = sqlite3_column_int64(iface_stmt, 0);
2497+
cbm_store_bfs(store, iface_method_id, "inbound", call_types, 5, 1,
2498+
EDGE_QUERY_MAX, &all_tr[tr_count]);
2499+
if (all_tr[tr_count].visited_count > 0) tr_count++;
2500+
}
2501+
sqlite3_finalize(iface_stmt);
2502+
}
2503+
}
2504+
}
24232505
}
24242506
/* Build calls array from all BFS results */
24252507
yyjson_mut_val *calls_arr = yyjson_mut_arr(doc);

src/pipeline/pass_definitions.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,12 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
253253
def->qualified_name, def->file_path ? def->file_path : rel,
254254
(int)def->start_line, (int)def->end_line, props);
255255

256-
/* Register callable symbols in the registry */
256+
/* Register callable symbols in the registry.
257+
* Interface is included so that C#/Java INHERITS edges can resolve
258+
* base type names like "IExamService" to Interface nodes. */
257259
if (node_id > 0 && def->label &&
258260
(strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
259-
strcmp(def->label, "Class") == 0)) {
261+
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0)) {
260262
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
261263
}
262264

src/pipeline/pass_parallel.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
612612
}
613613

614614
if (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
615-
strcmp(def->label, "Class") == 0) {
615+
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0) {
616616
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
617617
reg_entries++;
618618
}

src/pipeline/pass_semantic.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,9 @@ int cbm_pipeline_pass_semantic(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
374374
/* INHERITS: base_classes */
375375
if (def->base_classes) {
376376
for (int b = 0; def->base_classes[b]; b++) {
377+
const char *base_name = def->base_classes[b];
377378
const char *base_qn =
378-
resolve_as_class(ctx->registry, def->base_classes[b], module_qn, imp_keys,
379+
resolve_as_class(ctx->registry, base_name, module_qn, imp_keys,
379380
imp_vals, imp_count);
380381
if (!base_qn) {
381382
continue;
@@ -386,6 +387,29 @@ int cbm_pipeline_pass_semantic(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
386387
continue;
387388
}
388389

390+
/* C# interface preference: if the resolved node is a Class but the
391+
* base type name follows C# interface convention (I + uppercase),
392+
* search for an Interface node with the same name and prefer it.
393+
* This handles cross-language name collisions (e.g. C++ ILogger class
394+
* vs C# ILogger interface) that cause INHERITS to miss interfaces. */
395+
if (base_node->label && strcmp(base_node->label, "Class") == 0 &&
396+
base_name[0] == 'I' && base_name[1] >= 'A' && base_name[1] <= 'Z') {
397+
const char **candidates = NULL;
398+
int cand_count = 0;
399+
cbm_registry_find_by_name(ctx->registry, base_name, &candidates, &cand_count);
400+
for (int ci = 0; ci < cand_count; ci++) {
401+
const char *cl = cbm_registry_label_of(ctx->registry, candidates[ci]);
402+
if (cl && strcmp(cl, "Interface") == 0) {
403+
const cbm_gbuf_node_t *iface_node =
404+
cbm_gbuf_find_by_qn(ctx->gbuf, candidates[ci]);
405+
if (iface_node && iface_node->id != node->id) {
406+
base_node = iface_node;
407+
break;
408+
}
409+
}
410+
}
411+
}
412+
389413
cbm_gbuf_insert_edge(ctx->gbuf, node->id, base_node->id, "INHERITS", "{}");
390414
inherits_count++;
391415
}

0 commit comments

Comments
 (0)