Skip to content

Commit 6a45196

Browse files
author
Your Name
committed
fix(mcp+extraction): C# class has_method + C# channel detection
Fix A — Class node 0-degree early exit: The crash guard that returns early for nodes with 0 CALLS edges was incorrectly catching Class/Interface nodes that have DEFINES_METHOD and INHERITS edges (cbm_store_node_degree only counts CALLS). Re-add the is_class_like exemption so Class nodes always proceed to DEFINES_METHOD resolution. Cap method resolution to 5 methods to prevent excessive BFS. Fix A2 — has_method uses Class node ID: The DEFINES_METHOD BFS was using method start_ids (from class resolution) as the BFS root, but DEFINES_METHOD edges go FROM the Class TO Methods. Use the original Class node ID for the has_method query. Result: 30 methods found (GitNexus: 29), extends chain shown. Fix B1 — Add .cs to channel detection file filter: Channel detection SQL now includes .cs files alongside JS/TS/Python. Fix B2 — C# channel extraction with constant resolution: New cbm_extract_csharp_channels() in httplink.c that handles: - const string CONSTANT = "value" → builds name-to-value map - .Emit(CONSTANT, ...) → resolves to string value, marks as emit - .OnRequest<T>(CONSTANT, ...) → resolves to string value, marks as listen - .Emit("literal", ...) → direct string literal matching Result: 73 channel references, 35 unique channels in C# repo (was 0).
1 parent 9e1dc6d commit 6a45196

3 files changed

Lines changed: 147 additions & 13 deletions

File tree

src/mcp/mcp.c

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,11 +1774,14 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
17741774
cbm_store_find_edges_by_source_type(store, nodes[best_idx].id, "DEFINES_METHOD",
17751775
&dm_edges, &dm_count);
17761776
if (dm_count > 0) {
1777-
start_ids = malloc((size_t)dm_count * sizeof(int64_t));
1778-
for (int i = 0; i < dm_count; i++) {
1777+
/* Cap at 5 methods to prevent excessive BFS calls (each method
1778+
* spawns ~6 BFS queries across edge type categories) */
1779+
int use_count = dm_count > 5 ? 5 : dm_count;
1780+
start_ids = malloc((size_t)use_count * sizeof(int64_t));
1781+
for (int i = 0; i < use_count; i++) {
17791782
start_ids[i] = dm_edges[i].target_id;
17801783
}
1781-
start_id_count = dm_count;
1784+
start_id_count = use_count;
17821785
}
17831786
/* Free edge data */
17841787
for (int i = 0; i < dm_count; i++) {
@@ -1842,8 +1845,11 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
18421845
int in_deg = 0;
18431846
int out_deg = 0;
18441847
cbm_store_node_degree(store, nodes[best_idx].id, &in_deg, &out_deg);
1845-
if (in_deg == 0 && out_deg == 0) {
1846-
/* No edges — return basic info */
1848+
if (in_deg == 0 && out_deg == 0 && !is_class_like) {
1849+
/* No CALLS edges and not a Class — return basic info.
1850+
* Class/Interface nodes skip this check because they have
1851+
* DEFINES_METHOD and INHERITS edges that aren't counted by
1852+
* cbm_store_node_degree (which only counts CALLS). */
18471853
char *json = yy_doc_to_str(doc);
18481854
yyjson_mut_doc_free(doc);
18491855
free(start_ids);
@@ -2013,12 +2019,14 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
20132019
yyjson_mut_obj_add_val(doc, outgoing, "calls", calls_arr);
20142020
}
20152021

2016-
/* Outgoing DEFINES_METHOD (for Classes) */
2022+
/* Outgoing DEFINES_METHOD (for Classes).
2023+
* Use the original Class node ID, not start_ids (which are method IDs).
2024+
* DEFINES_METHOD edges go FROM the Class TO its Methods. */
20172025
{
20182026
int saved_tr = tr_count;
2019-
for (int s = 0; s < start_id_count && tr_count < MAX_TR; s++) {
2027+
if (is_class_like && tr_count < MAX_TR) {
20202028
const char *dm_types[] = {"DEFINES_METHOD"};
2021-
cbm_store_bfs(store, start_ids[s], "outbound", dm_types, 1, 1, EDGE_QUERY_MAX,
2029+
cbm_store_bfs(store, nodes[best_idx].id, "outbound", dm_types, 1, 1, 30,
20222030
&all_tr[tr_count]);
20232031
tr_count++;
20242032
}

src/pipeline/httplink.c

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,3 +1995,119 @@ int cbm_extract_channels(const char *source, cbm_channel_match_t *out, int max_o
19951995
cbm_regfree(&re);
19961996
return count;
19971997
}
1998+
1999+
/* ── C# channel extraction: Socket.IO with constant resolution ─── */
2000+
2001+
/* Extract channels from C# source that uses constant names for event strings.
2002+
* Pattern: _socket.Emit(CONSTANT_NAME, data) / _socket.OnRequest<T>(CONSTANT_NAME, ...)
2003+
* Resolves constants via: const string CONSTANT_NAME = "ActualChannelName"; */
2004+
int cbm_extract_csharp_channels(const char *source, cbm_channel_match_t *out, int max_out) {
2005+
if (!source || !*source) return 0;
2006+
2007+
/* Pass 1: Collect const string mappings: name → value */
2008+
typedef struct { char name[128]; char value[256]; } const_map_t;
2009+
const_map_t cmap[128];
2010+
int cmap_count = 0;
2011+
2012+
cbm_regex_t re_const;
2013+
if (cbm_regcomp(&re_const,
2014+
"const[[:space:]]+string[[:space:]]+([A-Z_][A-Z_0-9]*)[[:space:]]*=[[:space:]]*\"([^\"]{1,128})\"",
2015+
CBM_REG_EXTENDED) == 0) {
2016+
const char *p = source;
2017+
cbm_regmatch_t cm[3];
2018+
while (cmap_count < 128 && cbm_regexec(&re_const, p, 3, cm, 0) == 0) {
2019+
int nlen = cm[1].rm_eo - cm[1].rm_so;
2020+
int vlen = cm[2].rm_eo - cm[2].rm_so;
2021+
if (nlen > 0 && nlen < 128 && vlen > 0 && vlen < 256) {
2022+
memcpy(cmap[cmap_count].name, p + cm[1].rm_so, (size_t)nlen);
2023+
cmap[cmap_count].name[nlen] = '\0';
2024+
memcpy(cmap[cmap_count].value, p + cm[2].rm_so, (size_t)vlen);
2025+
cmap[cmap_count].value[vlen] = '\0';
2026+
cmap_count++;
2027+
}
2028+
p += cm[0].rm_eo;
2029+
}
2030+
cbm_regfree(&re_const);
2031+
}
2032+
2033+
/* Pass 2: Find .Emit( and .OnRequest patterns */
2034+
int count = 0;
2035+
2036+
/* Pattern: .Emit(IDENTIFIER or .OnRequest<...>(IDENTIFIER */
2037+
cbm_regex_t re_emit;
2038+
if (cbm_regcomp(&re_emit,
2039+
"\\.(Emit|OnRequest)[^(]*\\([[:space:]]*([A-Z_][A-Z_0-9]*)",
2040+
CBM_REG_EXTENDED) == 0) {
2041+
const char *p = source;
2042+
cbm_regmatch_t em[3];
2043+
while (count < max_out && cbm_regexec(&re_emit, p, 3, em, 0) == 0) {
2044+
int mlen = em[1].rm_eo - em[1].rm_so;
2045+
char method[16];
2046+
if (mlen >= (int)sizeof(method)) mlen = (int)sizeof(method) - 1;
2047+
memcpy(method, p + em[1].rm_so, (size_t)mlen);
2048+
method[mlen] = '\0';
2049+
2050+
int ilen = em[2].rm_eo - em[2].rm_so;
2051+
char ident[128];
2052+
if (ilen >= (int)sizeof(ident)) ilen = (int)sizeof(ident) - 1;
2053+
memcpy(ident, p + em[2].rm_so, (size_t)ilen);
2054+
ident[ilen] = '\0';
2055+
2056+
/* Resolve constant to string value */
2057+
const char *resolved = NULL;
2058+
for (int i = 0; i < cmap_count; i++) {
2059+
if (strcmp(cmap[i].name, ident) == 0) {
2060+
resolved = cmap[i].value;
2061+
break;
2062+
}
2063+
}
2064+
2065+
if (resolved) {
2066+
strncpy(out[count].channel, resolved, sizeof(out[count].channel) - 1);
2067+
out[count].channel[sizeof(out[count].channel) - 1] = '\0';
2068+
2069+
if (strcmp(method, "Emit") == 0) {
2070+
strncpy(out[count].direction, "emit", sizeof(out[count].direction) - 1);
2071+
} else {
2072+
strncpy(out[count].direction, "listen", sizeof(out[count].direction) - 1);
2073+
}
2074+
strncpy(out[count].transport, "socketio", sizeof(out[count].transport) - 1);
2075+
count++;
2076+
}
2077+
p += em[0].rm_eo;
2078+
}
2079+
cbm_regfree(&re_emit);
2080+
}
2081+
2082+
/* Also match direct string literal patterns: .Emit("ChannelName" */
2083+
cbm_regex_t re_literal;
2084+
if (cbm_regcomp(&re_literal,
2085+
"\\.(Emit|On|OnRequest)[^(]*\\([[:space:]]*\"([^\"]{1,128})\"",
2086+
CBM_REG_EXTENDED) == 0) {
2087+
const char *p = source;
2088+
cbm_regmatch_t lm[3];
2089+
while (count < max_out && cbm_regexec(&re_literal, p, 3, lm, 0) == 0) {
2090+
int mlen = lm[1].rm_eo - lm[1].rm_so;
2091+
char method[16];
2092+
if (mlen >= (int)sizeof(method)) mlen = (int)sizeof(method) - 1;
2093+
memcpy(method, p + lm[1].rm_so, (size_t)mlen);
2094+
method[mlen] = '\0';
2095+
2096+
int clen = lm[2].rm_eo - lm[2].rm_so;
2097+
strncpy(out[count].channel, p + lm[2].rm_so, (size_t)(clen < 255 ? clen : 255));
2098+
out[count].channel[clen < 255 ? clen : 255] = '\0';
2099+
2100+
if (strcmp(method, "Emit") == 0) {
2101+
strncpy(out[count].direction, "emit", sizeof(out[count].direction) - 1);
2102+
} else {
2103+
strncpy(out[count].direction, "listen", sizeof(out[count].direction) - 1);
2104+
}
2105+
strncpy(out[count].transport, "socketio", sizeof(out[count].transport) - 1);
2106+
count++;
2107+
p += lm[0].rm_eo;
2108+
}
2109+
cbm_regfree(&re_literal);
2110+
}
2111+
2112+
return count;
2113+
}

src/store/store.c

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4956,13 +4956,14 @@ void cbm_store_free_process_steps(cbm_process_step_t *arr, int count) {
49564956

49574957
/* ── Channels (cross-service message tracing) ────────────────────── */
49584958

4959-
/* Forward declaration of channel extractor from httplink.c */
4959+
/* Forward declaration of channel extractors from httplink.c */
49604960
typedef struct {
49614961
char channel[256];
49624962
char direction[8];
49634963
char transport[32];
49644964
} cbm_channel_match_t;
49654965
int cbm_extract_channels(const char *source, cbm_channel_match_t *out, int max_out);
4966+
int cbm_extract_csharp_channels(const char *source, cbm_channel_match_t *out, int max_out);
49664967

49674968
int cbm_store_detect_channels(cbm_store_t *s, const char *project, const char *repo_path) {
49684969
if (!s || !s->db || !project || !repo_path) return 0;
@@ -4972,11 +4973,12 @@ int cbm_store_detect_channels(cbm_store_t *s, const char *project, const char *r
49724973
snprintf(del, sizeof(del), "DELETE FROM channels WHERE project = '%s'", project);
49734974
exec_sql(s, del);
49744975

4975-
/* Find all JS/TS Function/Method nodes with source file references */
4976+
/* Find all Function/Method nodes with source file references in supported languages */
49764977
const char *sql = "SELECT id, name, file_path, start_line, end_line FROM nodes "
4977-
"WHERE project = ?1 AND label IN ('Function','Method','Module') "
4978+
"WHERE project = ?1 AND label IN ('Function','Method','Module','Class') "
49784979
"AND (file_path LIKE '%.ts' OR file_path LIKE '%.js' "
4979-
"OR file_path LIKE '%.tsx' OR file_path LIKE '%.py')";
4980+
"OR file_path LIKE '%.tsx' OR file_path LIKE '%.py' "
4981+
"OR file_path LIKE '%.cs')";
49804982
sqlite3_stmt *stmt = NULL;
49814983
if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
49824984
bind_text(stmt, 1, project);
@@ -5029,7 +5031,15 @@ int cbm_store_detect_channels(cbm_store_t *s, const char *project, const char *r
50295031
if (source) {
50305032
source[src_len] = '\0';
50315033
cbm_channel_match_t matches[64];
5032-
int mc = cbm_extract_channels(source, matches, 64);
5034+
int mc = 0;
5035+
/* Use language-appropriate extractor */
5036+
bool is_cs = fpath && (strstr(fpath, ".cs") != NULL &&
5037+
strstr(fpath, ".css") == NULL);
5038+
if (is_cs) {
5039+
mc = cbm_extract_csharp_channels(source, matches, 64);
5040+
} else {
5041+
mc = cbm_extract_channels(source, matches, 64);
5042+
}
50335043
for (int i = 0; i < mc && ins; i++) {
50345044
sqlite3_reset(ins);
50355045
bind_text(ins, 1, project);

0 commit comments

Comments
 (0)