Skip to content

Commit 81385d0

Browse files
author
test
committed
Add gRPC/GraphQL/tRPC service detection with protobuf Route extraction
Pipeline: gRPC/GraphQL/tRPC client libs in service_patterns, edge emitters in pass_parallel, __grpc__ Route nodes from .proto service defs in pass_route_nodes, cross-repo matchers in pass_cross_repo. Also adds MQTT, NATS, Dapr async patterns. Protobuf extraction: service as Class, rpc as Function with rpc_name fallback in func_name_node. Route nodes created by line-range matching rpc Functions to enclosing service Classes. Tested: Online Boutique protos produce 16 gRPC Route nodes.
1 parent b6eefe0 commit 81385d0

9 files changed

Lines changed: 488 additions & 23 deletions

File tree

internal/cbm/extract_defs.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,12 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
188188

189189
// Get "name" field from a node
190190
static TSNode func_name_node(TSNode node) {
191-
return ts_node_child_by_field_name(node, TS_FIELD("name"));
191+
TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name"));
192+
if (ts_node_is_null(name)) {
193+
/* Protobuf rpc: name is in rpc_name child, not "name" field */
194+
name = cbm_find_child_by_kind(node, "rpc_name");
195+
}
196+
return name;
192197
}
193198

194199
// Lua: resolve anonymous function assignment name from parent assignment_statement.
@@ -1820,6 +1825,16 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
18201825
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SWIFT) {
18211826
name_node = cbm_find_child_by_kind(node, "type_identifier");
18221827
}
1828+
// Protobuf: service_name / message_name / enum_name children
1829+
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PROTOBUF) {
1830+
name_node = cbm_find_child_by_kind(node, "service_name");
1831+
if (ts_node_is_null(name_node)) {
1832+
name_node = cbm_find_child_by_kind(node, "message_name");
1833+
}
1834+
if (ts_node_is_null(name_node)) {
1835+
name_node = cbm_find_child_by_kind(node, "enum_name");
1836+
}
1837+
}
18231838
if (ts_node_is_null(name_node)) {
18241839
return;
18251840
}

internal/cbm/lang_specs.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,8 @@ static const char *cmake_module_types[] = {"source_file", NULL};
627627
static const char *cmake_call_types[] = {"normal_command", NULL};
628628

629629
// ==================== PROTOBUF ====================
630-
static const char *protobuf_class_types[] = {"message", "enum", NULL};
630+
static const char *protobuf_class_types[] = {"message", "enum", "service", NULL};
631+
static const char *protobuf_func_types[] = {"rpc", NULL};
631632
static const char *protobuf_module_types[] = {"source_file", NULL};
632633
static const char *protobuf_field_types[] = {"field", "map_field", "oneof_field", NULL};
633634
static const char *protobuf_import_types[] = {"import", NULL};
@@ -983,7 +984,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
983984
empty_types, NULL, NULL},
984985

985986
// CBM_LANG_PROTOBUF
986-
{CBM_LANG_PROTOBUF, empty_types, protobuf_class_types, protobuf_field_types,
987+
{CBM_LANG_PROTOBUF, protobuf_func_types, protobuf_class_types, protobuf_field_types,
987988
protobuf_module_types, empty_types, protobuf_import_types, empty_types, empty_types,
988989
empty_types, empty_types, empty_types, NULL, empty_types, NULL, NULL},
989990

internal/cbm/service_patterns.c

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,28 @@ static const lib_pattern_t async_libraries[] = {
234234
/* Scala */
235235
{"Alpakka", CBM_SVC_ASYNC, "alpakka"},
236236

237+
/* MQTT */
238+
{"mqtt", CBM_SVC_ASYNC, "mqtt"},
239+
{"paho.mqtt", CBM_SVC_ASYNC, "mqtt"},
240+
{"MQTTClient", CBM_SVC_ASYNC, "mqtt"},
241+
{"mosquitto", CBM_SVC_ASYNC, "mqtt"},
242+
{"asyncio_mqtt", CBM_SVC_ASYNC, "mqtt"},
243+
{"gmqtt", CBM_SVC_ASYNC, "mqtt"},
244+
{"rumqttc", CBM_SVC_ASYNC, "mqtt"},
245+
246+
/* NATS */
247+
{"nats.go", CBM_SVC_ASYNC, "nats"},
248+
{"nats-py", CBM_SVC_ASYNC, "nats"},
249+
{"nats.ws", CBM_SVC_ASYNC, "nats"},
250+
{"nats.java", CBM_SVC_ASYNC, "nats"},
251+
{"nats.net", CBM_SVC_ASYNC, "nats"},
252+
{"async-nats", CBM_SVC_ASYNC, "nats"},
253+
{"nats.rs", CBM_SVC_ASYNC, "nats"},
254+
255+
/* Dapr pub/sub */
256+
{"dapr.clients.grpc", CBM_SVC_ASYNC, "dapr"},
257+
{"DaprClient", CBM_SVC_ASYNC, "dapr"},
258+
237259
{NULL, CBM_SVC_NONE, NULL},
238260
};
239261

@@ -351,6 +373,88 @@ static const lib_pattern_t route_reg_libraries[] = {
351373
{NULL, CBM_SVC_NONE, NULL},
352374
};
353375

376+
/* gRPC client libraries — protobuf stub invocations */
377+
static const lib_pattern_t grpc_libraries[] = {
378+
/* Go */
379+
{"google.golang.org/grpc", CBM_SVC_GRPC, NULL},
380+
{"grpc.Dial", CBM_SVC_GRPC, NULL},
381+
{"grpc.NewClient", CBM_SVC_GRPC, NULL},
382+
{"grpc.DialContext", CBM_SVC_GRPC, NULL},
383+
384+
/* Python */
385+
{"grpc.insecure_channel", CBM_SVC_GRPC, NULL},
386+
{"grpc.secure_channel", CBM_SVC_GRPC, NULL},
387+
{"grpcio", CBM_SVC_GRPC, NULL},
388+
{"grpc.aio", CBM_SVC_GRPC, NULL},
389+
390+
/* Java/Kotlin */
391+
{"io.grpc", CBM_SVC_GRPC, NULL},
392+
{"ManagedChannelBuilder", CBM_SVC_GRPC, NULL},
393+
{"ManagedChannel", CBM_SVC_GRPC, NULL},
394+
{"newBlockingStub", CBM_SVC_GRPC, NULL},
395+
{"newFutureStub", CBM_SVC_GRPC, NULL},
396+
397+
/* C# */
398+
{"Grpc.Net.Client", CBM_SVC_GRPC, NULL},
399+
{"GrpcChannel", CBM_SVC_GRPC, NULL},
400+
{"Grpc.Core", CBM_SVC_GRPC, NULL},
401+
402+
/* JS/TS */
403+
{"@grpc/grpc-js", CBM_SVC_GRPC, NULL},
404+
{"grpc-web", CBM_SVC_GRPC, NULL},
405+
406+
/* Rust */
407+
{"tonic", CBM_SVC_GRPC, NULL},
408+
409+
/* Dart/Flutter */
410+
{"package:grpc", CBM_SVC_GRPC, NULL},
411+
412+
{NULL, CBM_SVC_NONE, NULL},
413+
};
414+
415+
/* GraphQL client libraries */
416+
static const lib_pattern_t graphql_libraries[] = {
417+
/* JS/TS */
418+
{"graphql-request", CBM_SVC_GRAPHQL, NULL},
419+
{"@apollo/client", CBM_SVC_GRAPHQL, NULL},
420+
{"apollo-client", CBM_SVC_GRAPHQL, NULL},
421+
{"urql", CBM_SVC_GRAPHQL, NULL},
422+
{"graphql-tag", CBM_SVC_GRAPHQL, NULL},
423+
424+
/* Python */
425+
{"gql", CBM_SVC_GRAPHQL, NULL},
426+
{"sgqlc", CBM_SVC_GRAPHQL, NULL},
427+
{"graphene", CBM_SVC_GRAPHQL, NULL},
428+
429+
/* Java */
430+
{"graphql-java", CBM_SVC_GRAPHQL, NULL},
431+
{"DgsQueryExecutor", CBM_SVC_GRAPHQL, NULL},
432+
433+
/* Go */
434+
{"graphql-go", CBM_SVC_GRAPHQL, NULL},
435+
{"gqlgen", CBM_SVC_GRAPHQL, NULL},
436+
437+
/* Ruby */
438+
{"graphql-ruby", CBM_SVC_GRAPHQL, NULL},
439+
440+
/* Rust */
441+
{"async-graphql", CBM_SVC_GRAPHQL, NULL},
442+
{"juniper", CBM_SVC_GRAPHQL, NULL},
443+
444+
{NULL, CBM_SVC_NONE, NULL},
445+
};
446+
447+
/* tRPC libraries (TypeScript only) */
448+
static const lib_pattern_t trpc_libraries[] = {
449+
{"@trpc/server", CBM_SVC_TRPC, NULL},
450+
{"@trpc/client", CBM_SVC_TRPC, NULL},
451+
{"@trpc/react-query", CBM_SVC_TRPC, NULL},
452+
{"createTRPCRouter", CBM_SVC_TRPC, NULL},
453+
{"createTRPCProxyClient", CBM_SVC_TRPC, NULL},
454+
455+
{NULL, CBM_SVC_NONE, NULL},
456+
};
457+
354458
/* Method suffix type (used by both route registration and HTTP client tables) */
355459
typedef struct {
356460
const char *suffix;
@@ -472,6 +576,21 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) {
472576
return p->kind;
473577
}
474578

579+
p = match_qn(resolved_qn, grpc_libraries);
580+
if (p) {
581+
return p->kind;
582+
}
583+
584+
p = match_qn(resolved_qn, graphql_libraries);
585+
if (p) {
586+
return p->kind;
587+
}
588+
589+
p = match_qn(resolved_qn, trpc_libraries);
590+
if (p) {
591+
return p->kind;
592+
}
593+
475594
return CBM_SVC_NONE;
476595
}
477596

internal/cbm/service_patterns.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ typedef enum {
1818
CBM_SVC_ASYNC = 2, /* Async dispatch (message broker, task queue) */
1919
CBM_SVC_CONFIG = 3, /* Config/env accessor */
2020
CBM_SVC_ROUTE_REG = 4, /* Route registration (router.GET, app.get, Route::post) */
21+
CBM_SVC_GRPC = 5, /* gRPC client call (protobuf stub invocation) */
22+
CBM_SVC_GRAPHQL = 6, /* GraphQL client query/mutation */
23+
CBM_SVC_TRPC = 7, /* tRPC client procedure call */
2124
} cbm_svc_kind_t;
2225

2326
/* Initialize the pattern lookup tables. Call once at startup. Thread-safe after init. */

src/mcp/mcp.c

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,24 +1671,22 @@ static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, cons
16711671
/* Append cross_repo_links summary to architecture JSON if CROSS_* edges exist. */
16721672
static void append_cross_repo_summary(yyjson_mut_doc *doc, yyjson_mut_val *root,
16731673
const cbm_schema_info_t *schema) {
1674-
int cross_http = 0;
1675-
int cross_async = 0;
1676-
int cross_channel = 0;
1677-
for (int i = 0; i < schema->edge_type_count; i++) {
1678-
if (strcmp(schema->edge_types[i].type, "CROSS_HTTP_CALLS") == 0) {
1679-
cross_http = schema->edge_types[i].count;
1680-
} else if (strcmp(schema->edge_types[i].type, "CROSS_ASYNC_CALLS") == 0) {
1681-
cross_async = schema->edge_types[i].count;
1682-
} else if (strcmp(schema->edge_types[i].type, "CROSS_CHANNEL") == 0) {
1683-
cross_channel = schema->edge_types[i].count;
1674+
/* Scan edge types for any CROSS_* edges and sum them */
1675+
int cross_total = 0;
1676+
yyjson_mut_val *cr = yyjson_mut_obj(doc);
1677+
static const char *cross_types[] = {"CROSS_HTTP_CALLS", "CROSS_ASYNC_CALLS",
1678+
"CROSS_CHANNEL", "CROSS_GRPC_CALLS",
1679+
"CROSS_GRAPHQL_CALLS", "CROSS_TRPC_CALLS"};
1680+
for (int t = 0; t < (int)(sizeof(cross_types) / sizeof(cross_types[0])); t++) {
1681+
for (int i = 0; i < schema->edge_type_count; i++) {
1682+
if (strcmp(schema->edge_types[i].type, cross_types[t]) == 0) {
1683+
yyjson_mut_obj_add_int(doc, cr, cross_types[t], schema->edge_types[i].count);
1684+
cross_total += schema->edge_types[i].count;
1685+
break;
1686+
}
16841687
}
16851688
}
1686-
int cross_total = cross_http + cross_async + cross_channel;
16871689
if (cross_total > 0) {
1688-
yyjson_mut_val *cr = yyjson_mut_obj(doc);
1689-
yyjson_mut_obj_add_int(doc, cr, "cross_http_calls", cross_http);
1690-
yyjson_mut_obj_add_int(doc, cr, "cross_async_calls", cross_async);
1691-
yyjson_mut_obj_add_int(doc, cr, "cross_channel", cross_channel);
16921690
yyjson_mut_obj_add_int(doc, cr, "total", cross_total);
16931691
yyjson_mut_obj_add_val(doc, root, "cross_repo_links", cr);
16941692
}
@@ -2113,7 +2111,8 @@ static char *handle_cross_repo_mode(const char *repo_path, const char *args) {
21132111
free(targets);
21142112
yyjson_doc_free(jdoc);
21152113

2116-
int total = result.http_edges + result.async_edges + result.channel_edges;
2114+
int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
2115+
result.graphql_edges + result.trpc_edges;
21172116
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
21182117
yyjson_mut_val *root = yyjson_mut_obj(doc);
21192118
yyjson_mut_doc_set_root(doc, root);
@@ -2124,6 +2123,9 @@ static char *handle_cross_repo_mode(const char *repo_path, const char *args) {
21242123
yyjson_mut_obj_add_int(doc, root, "cross_http_calls", result.http_edges);
21252124
yyjson_mut_obj_add_int(doc, root, "cross_async_calls", result.async_edges);
21262125
yyjson_mut_obj_add_int(doc, root, "cross_channel", result.channel_edges);
2126+
yyjson_mut_obj_add_int(doc, root, "cross_grpc_calls", result.grpc_edges);
2127+
yyjson_mut_obj_add_int(doc, root, "cross_graphql_calls", result.graphql_edges);
2128+
yyjson_mut_obj_add_int(doc, root, "cross_trpc_calls", result.trpc_edges);
21272129
yyjson_mut_obj_add_int(doc, root, "total_cross_edges", total);
21282130
yyjson_mut_obj_add_real(doc, root, "elapsed_ms", result.elapsed_ms);
21292131

src/pipeline/pass_cross_repo.c

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ static void delete_cross_edges(cbm_store_t *store, const char *project) {
109109
cbm_store_delete_edges_by_type(store, project, "CROSS_HTTP_CALLS");
110110
cbm_store_delete_edges_by_type(store, project, "CROSS_ASYNC_CALLS");
111111
cbm_store_delete_edges_by_type(store, project, "CROSS_CHANNEL");
112+
cbm_store_delete_edges_by_type(store, project, "CROSS_GRPC_CALLS");
113+
cbm_store_delete_edges_by_type(store, project, "CROSS_GRAPHQL_CALLS");
114+
cbm_store_delete_edges_by_type(store, project, "CROSS_TRPC_CALLS");
112115
}
113116

114117
/* Insert a CROSS_* edge into a store. */
@@ -461,6 +464,90 @@ static int match_channels(cbm_store_t *src_store, const char *src_project, cbm_s
461464
return count;
462465
}
463466

467+
/* ── Phase D: Generic route-type matcher (gRPC, GraphQL, tRPC) ──── */
468+
469+
/* Look up a node's qualified_name by id. Returns true if found. */
470+
static bool lookup_node_qn(struct sqlite3 *db, int64_t node_id, char *out, size_t out_sz) {
471+
out[0] = '\0';
472+
sqlite3_stmt *st = NULL;
473+
if (sqlite3_prepare_v2(db, "SELECT qualified_name FROM nodes WHERE id = ?1", CBM_NOT_FOUND, &st,
474+
NULL) != SQLITE_OK) {
475+
return false;
476+
}
477+
sqlite3_bind_int64(st, SKIP_ONE, node_id);
478+
bool found = false;
479+
if (sqlite3_step(st) == SQLITE_ROW) {
480+
const char *qn = (const char *)sqlite3_column_text(st, 0);
481+
if (qn) {
482+
snprintf(out, out_sz, "%s", qn);
483+
found = true;
484+
}
485+
}
486+
sqlite3_finalize(st);
487+
return found;
488+
}
489+
490+
/* Match edges of a given type against Route nodes with a given QN prefix.
491+
* Reuses the same infrastructure as HTTP/async matching. */
492+
static int match_typed_routes(cbm_store_t *src_store, const char *src_project,
493+
cbm_store_t *tgt_store, const char *tgt_project,
494+
const char *edge_type, const char *svc_key, const char *method_key,
495+
const char *cross_edge_type) {
496+
struct sqlite3 *src_db = cbm_store_get_db(src_store);
497+
if (!src_db) {
498+
return 0;
499+
}
500+
501+
char sql[CBM_SZ_256];
502+
snprintf(sql, sizeof(sql),
503+
"SELECT e.source_id, e.target_id, e.properties FROM edges e "
504+
"WHERE e.project = ?1 AND e.type = '%s'",
505+
edge_type);
506+
507+
sqlite3_stmt *s = NULL;
508+
if (sqlite3_prepare_v2(src_db, sql, CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
509+
return 0;
510+
}
511+
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
512+
513+
int count = 0;
514+
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
515+
int64_t caller_id = sqlite3_column_int64(s, 0);
516+
int64_t route_id = sqlite3_column_int64(s, SKIP_ONE);
517+
const char *props = (const char *)sqlite3_column_text(s, PAIR_LEN);
518+
519+
char svc_val[CBM_SZ_256] = {0};
520+
char meth_val[CBM_SZ_256] = {0};
521+
json_str_prop(props, svc_key, svc_val, sizeof(svc_val));
522+
json_str_prop(props, method_key, meth_val, sizeof(meth_val));
523+
if (!svc_val[0] && !meth_val[0]) {
524+
continue;
525+
}
526+
527+
/* Look up the Route QN from the target node (already points to the Route). */
528+
char route_qn[CR_QN_BUF] = {0};
529+
if (!lookup_node_qn(src_db, route_id, route_qn, sizeof(route_qn))) {
530+
continue;
531+
}
532+
533+
char handler_name[CBM_SZ_256] = {0};
534+
char handler_file[CBM_SZ_512] = {0};
535+
int64_t handler_id =
536+
find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
537+
handler_file, sizeof(handler_file));
538+
if (handler_id == 0) {
539+
continue;
540+
}
541+
542+
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
543+
tgt_store, tgt_project, handler_id, route_qn, handler_name,
544+
handler_file, svc_val, svc_key, cross_edge_type);
545+
count++;
546+
}
547+
sqlite3_finalize(s);
548+
return count;
549+
}
550+
464551
/* ── Collect target projects ─────────────────────────────────────── */
465552

466553
/* When target_projects = ["*"], scan the cache directory for all .db files. */
@@ -566,6 +653,13 @@ cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **t
566653
result.http_edges += match_http_routes(src_store, project, tgt_store, tgt);
567654
result.async_edges += match_async_routes(src_store, project, tgt_store, tgt);
568655
result.channel_edges += match_channels(src_store, project, tgt_store, tgt);
656+
result.grpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "GRPC_CALLS",
657+
"service", "method", "CROSS_GRPC_CALLS");
658+
result.graphql_edges +=
659+
match_typed_routes(src_store, project, tgt_store, tgt, "GRAPHQL_CALLS", "operation",
660+
"operation", "CROSS_GRAPHQL_CALLS");
661+
result.trpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "TRPC_CALLS",
662+
"procedure", "procedure", "CROSS_TRPC_CALLS");
569663
result.projects_scanned++;
570664

571665
cbm_store_close(tgt_store);
@@ -582,10 +676,9 @@ cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **t
582676
result.elapsed_ms = ((double)(t1.tv_sec - t0.tv_sec) * CR_MS_PER_SEC) +
583677
((double)(t1.tv_nsec - t0.tv_nsec) / CR_NS_PER_MS);
584678

585-
int total = result.http_edges + result.async_edges + result.channel_edges;
586-
cbm_log_info("cross_repo.done", "project", project, "http", cr_itoa(result.http_edges), "async",
587-
cr_itoa(result.async_edges), "channel", cr_itoa(result.channel_edges), "total",
588-
cr_itoa(total));
679+
int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
680+
result.graphql_edges + result.trpc_edges;
681+
cbm_log_info("cross_repo.done", "project", project, "total", cr_itoa(total));
589682

590683
return result;
591684
}

src/pipeline/pass_cross_repo.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ typedef struct {
1212
int http_edges; /* CROSS_HTTP_CALLS edges created */
1313
int async_edges; /* CROSS_ASYNC_CALLS edges created */
1414
int channel_edges; /* CROSS_CHANNEL edges created */
15+
int grpc_edges; /* CROSS_GRPC_CALLS edges created */
16+
int graphql_edges; /* CROSS_GRAPHQL_CALLS edges created */
17+
int trpc_edges; /* CROSS_TRPC_CALLS edges created */
1518
int projects_scanned;
1619
double elapsed_ms;
1720
} cbm_cross_repo_result_t;

0 commit comments

Comments
 (0)