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.
This commit is contained in:
test committed 2026-04-15 23:53:42 +02:00
1 parent b6eefe014c
commit 81385d08eb
9 files changed
+488 -23

No files matched your search

+16 -1
View File
@@ -188,7 +188,12 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
// Get "name" field from a node
static TSNode func_name_node(TSNode node) {
return ts_node_child_by_field_name(node, TS_FIELD("name"));
TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name"));
if (ts_node_is_null(name)) {
/* Protobuf rpc: name is in rpc_name child, not "name" field */
name = cbm_find_child_by_kind(node, "rpc_name");
}
return name;
}
// 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
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SWIFT) {
name_node = cbm_find_child_by_kind(node, "type_identifier");
}
// Protobuf: service_name / message_name / enum_name children
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PROTOBUF) {
name_node = cbm_find_child_by_kind(node, "service_name");
if (ts_node_is_null(name_node)) {
name_node = cbm_find_child_by_kind(node, "message_name");
}
if (ts_node_is_null(name_node)) {
name_node = cbm_find_child_by_kind(node, "enum_name");
}
}
if (ts_node_is_null(name_node)) {
return;
}
+3 -2
View File
@@ -627,7 +627,8 @@ static const char *cmake_module_types[] = {"source_file", NULL};
static const char *cmake_call_types[] = {"normal_command", NULL};
// ==================== PROTOBUF ====================
static const char *protobuf_class_types[] = {"message", "enum", NULL};
static const char *protobuf_class_types[] = {"message", "enum", "service", NULL};
static const char *protobuf_func_types[] = {"rpc", NULL};
static const char *protobuf_module_types[] = {"source_file", NULL};
static const char *protobuf_field_types[] = {"field", "map_field", "oneof_field", NULL};
static const char *protobuf_import_types[] = {"import", NULL};
@@ -983,7 +984,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
empty_types, NULL, NULL},
// CBM_LANG_PROTOBUF
{CBM_LANG_PROTOBUF, empty_types, protobuf_class_types, protobuf_field_types,
{CBM_LANG_PROTOBUF, protobuf_func_types, protobuf_class_types, protobuf_field_types,
protobuf_module_types, empty_types, protobuf_import_types, empty_types, empty_types,
empty_types, empty_types, empty_types, NULL, empty_types, NULL, NULL},
+119
View File
@@ -234,6 +234,28 @@ static const lib_pattern_t async_libraries[] = {
/* Scala */
{"Alpakka", CBM_SVC_ASYNC, "alpakka"},
/* MQTT */
{"mqtt", CBM_SVC_ASYNC, "mqtt"},
{"paho.mqtt", CBM_SVC_ASYNC, "mqtt"},
{"MQTTClient", CBM_SVC_ASYNC, "mqtt"},
{"mosquitto", CBM_SVC_ASYNC, "mqtt"},
{"asyncio_mqtt", CBM_SVC_ASYNC, "mqtt"},
{"gmqtt", CBM_SVC_ASYNC, "mqtt"},
{"rumqttc", CBM_SVC_ASYNC, "mqtt"},
/* NATS */
{"nats.go", CBM_SVC_ASYNC, "nats"},
{"nats-py", CBM_SVC_ASYNC, "nats"},
{"nats.ws", CBM_SVC_ASYNC, "nats"},
{"nats.java", CBM_SVC_ASYNC, "nats"},
{"nats.net", CBM_SVC_ASYNC, "nats"},
{"async-nats", CBM_SVC_ASYNC, "nats"},
{"nats.rs", CBM_SVC_ASYNC, "nats"},
/* Dapr pub/sub */
{"dapr.clients.grpc", CBM_SVC_ASYNC, "dapr"},
{"DaprClient", CBM_SVC_ASYNC, "dapr"},
{NULL, CBM_SVC_NONE, NULL},
};
@@ -351,6 +373,88 @@ static const lib_pattern_t route_reg_libraries[] = {
{NULL, CBM_SVC_NONE, NULL},
};
/* gRPC client libraries — protobuf stub invocations */
static const lib_pattern_t grpc_libraries[] = {
/* Go */
{"google.golang.org/grpc", CBM_SVC_GRPC, NULL},
{"grpc.Dial", CBM_SVC_GRPC, NULL},
{"grpc.NewClient", CBM_SVC_GRPC, NULL},
{"grpc.DialContext", CBM_SVC_GRPC, NULL},
/* Python */
{"grpc.insecure_channel", CBM_SVC_GRPC, NULL},
{"grpc.secure_channel", CBM_SVC_GRPC, NULL},
{"grpcio", CBM_SVC_GRPC, NULL},
{"grpc.aio", CBM_SVC_GRPC, NULL},
/* Java/Kotlin */
{"io.grpc", CBM_SVC_GRPC, NULL},
{"ManagedChannelBuilder", CBM_SVC_GRPC, NULL},
{"ManagedChannel", CBM_SVC_GRPC, NULL},
{"newBlockingStub", CBM_SVC_GRPC, NULL},
{"newFutureStub", CBM_SVC_GRPC, NULL},
/* C# */
{"Grpc.Net.Client", CBM_SVC_GRPC, NULL},
{"GrpcChannel", CBM_SVC_GRPC, NULL},
{"Grpc.Core", CBM_SVC_GRPC, NULL},
/* JS/TS */
{"@grpc/grpc-js", CBM_SVC_GRPC, NULL},
{"grpc-web", CBM_SVC_GRPC, NULL},
/* Rust */
{"tonic", CBM_SVC_GRPC, NULL},
/* Dart/Flutter */
{"package:grpc", CBM_SVC_GRPC, NULL},
{NULL, CBM_SVC_NONE, NULL},
};
/* GraphQL client libraries */
static const lib_pattern_t graphql_libraries[] = {
/* JS/TS */
{"graphql-request", CBM_SVC_GRAPHQL, NULL},
{"@apollo/client", CBM_SVC_GRAPHQL, NULL},
{"apollo-client", CBM_SVC_GRAPHQL, NULL},
{"urql", CBM_SVC_GRAPHQL, NULL},
{"graphql-tag", CBM_SVC_GRAPHQL, NULL},
/* Python */
{"gql", CBM_SVC_GRAPHQL, NULL},
{"sgqlc", CBM_SVC_GRAPHQL, NULL},
{"graphene", CBM_SVC_GRAPHQL, NULL},
/* Java */
{"graphql-java", CBM_SVC_GRAPHQL, NULL},
{"DgsQueryExecutor", CBM_SVC_GRAPHQL, NULL},
/* Go */
{"graphql-go", CBM_SVC_GRAPHQL, NULL},
{"gqlgen", CBM_SVC_GRAPHQL, NULL},
/* Ruby */
{"graphql-ruby", CBM_SVC_GRAPHQL, NULL},
/* Rust */
{"async-graphql", CBM_SVC_GRAPHQL, NULL},
{"juniper", CBM_SVC_GRAPHQL, NULL},
{NULL, CBM_SVC_NONE, NULL},
};
/* tRPC libraries (TypeScript only) */
static const lib_pattern_t trpc_libraries[] = {
{"@trpc/server", CBM_SVC_TRPC, NULL},
{"@trpc/client", CBM_SVC_TRPC, NULL},
{"@trpc/react-query", CBM_SVC_TRPC, NULL},
{"createTRPCRouter", CBM_SVC_TRPC, NULL},
{"createTRPCProxyClient", CBM_SVC_TRPC, NULL},
{NULL, CBM_SVC_NONE, NULL},
};
/* Method suffix type (used by both route registration and HTTP client tables) */
typedef struct {
const char *suffix;
@@ -472,6 +576,21 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) {
return p->kind;
}
p = match_qn(resolved_qn, grpc_libraries);
if (p) {
return p->kind;
}
p = match_qn(resolved_qn, graphql_libraries);
if (p) {
return p->kind;
}
p = match_qn(resolved_qn, trpc_libraries);
if (p) {
return p->kind;
}
return CBM_SVC_NONE;
}
+3
View File
@@ -18,6 +18,9 @@ typedef enum {
CBM_SVC_ASYNC = 2, /* Async dispatch (message broker, task queue) */
CBM_SVC_CONFIG = 3, /* Config/env accessor */
CBM_SVC_ROUTE_REG = 4, /* Route registration (router.GET, app.get, Route::post) */
CBM_SVC_GRPC = 5, /* gRPC client call (protobuf stub invocation) */
CBM_SVC_GRAPHQL = 6, /* GraphQL client query/mutation */
CBM_SVC_TRPC = 7, /* tRPC client procedure call */
} cbm_svc_kind_t;
/* Initialize the pattern lookup tables. Call once at startup. Thread-safe after init. */
+18 -16
View File
@@ -1671,24 +1671,22 @@ static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, cons
/* Append cross_repo_links summary to architecture JSON if CROSS_* edges exist. */
static void append_cross_repo_summary(yyjson_mut_doc *doc, yyjson_mut_val *root,
const cbm_schema_info_t *schema) {
int cross_http = 0;
int cross_async = 0;
int cross_channel = 0;
for (int i = 0; i < schema->edge_type_count; i++) {
if (strcmp(schema->edge_types[i].type, "CROSS_HTTP_CALLS") == 0) {
cross_http = schema->edge_types[i].count;
} else if (strcmp(schema->edge_types[i].type, "CROSS_ASYNC_CALLS") == 0) {
cross_async = schema->edge_types[i].count;
} else if (strcmp(schema->edge_types[i].type, "CROSS_CHANNEL") == 0) {
cross_channel = schema->edge_types[i].count;
/* Scan edge types for any CROSS_* edges and sum them */
int cross_total = 0;
yyjson_mut_val *cr = yyjson_mut_obj(doc);
static const char *cross_types[] = {"CROSS_HTTP_CALLS", "CROSS_ASYNC_CALLS",
"CROSS_CHANNEL", "CROSS_GRPC_CALLS",
"CROSS_GRAPHQL_CALLS", "CROSS_TRPC_CALLS"};
for (int t = 0; t < (int)(sizeof(cross_types) / sizeof(cross_types[0])); t++) {
for (int i = 0; i < schema->edge_type_count; i++) {
if (strcmp(schema->edge_types[i].type, cross_types[t]) == 0) {
yyjson_mut_obj_add_int(doc, cr, cross_types[t], schema->edge_types[i].count);
cross_total += schema->edge_types[i].count;
break;
}
}
}
int cross_total = cross_http + cross_async + cross_channel;
if (cross_total > 0) {
yyjson_mut_val *cr = yyjson_mut_obj(doc);
yyjson_mut_obj_add_int(doc, cr, "cross_http_calls", cross_http);
yyjson_mut_obj_add_int(doc, cr, "cross_async_calls", cross_async);
yyjson_mut_obj_add_int(doc, cr, "cross_channel", cross_channel);
yyjson_mut_obj_add_int(doc, cr, "total", cross_total);
yyjson_mut_obj_add_val(doc, root, "cross_repo_links", cr);
}
@@ -2113,7 +2111,8 @@ static char *handle_cross_repo_mode(const char *repo_path, const char *args) {
free(targets);
yyjson_doc_free(jdoc);
int total = result.http_edges + result.async_edges + result.channel_edges;
int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
result.graphql_edges + result.trpc_edges;
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *root = yyjson_mut_obj(doc);
yyjson_mut_doc_set_root(doc, root);
@@ -2124,6 +2123,9 @@ static char *handle_cross_repo_mode(const char *repo_path, const char *args) {
yyjson_mut_obj_add_int(doc, root, "cross_http_calls", result.http_edges);
yyjson_mut_obj_add_int(doc, root, "cross_async_calls", result.async_edges);
yyjson_mut_obj_add_int(doc, root, "cross_channel", result.channel_edges);
yyjson_mut_obj_add_int(doc, root, "cross_grpc_calls", result.grpc_edges);
yyjson_mut_obj_add_int(doc, root, "cross_graphql_calls", result.graphql_edges);
yyjson_mut_obj_add_int(doc, root, "cross_trpc_calls", result.trpc_edges);
yyjson_mut_obj_add_int(doc, root, "total_cross_edges", total);
yyjson_mut_obj_add_real(doc, root, "elapsed_ms", result.elapsed_ms);
+97 -4
View File
@@ -109,6 +109,9 @@ static void delete_cross_edges(cbm_store_t *store, const char *project) {
cbm_store_delete_edges_by_type(store, project, "CROSS_HTTP_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_ASYNC_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_CHANNEL");
cbm_store_delete_edges_by_type(store, project, "CROSS_GRPC_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_GRAPHQL_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_TRPC_CALLS");
}
/* 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
return count;
}
/* ── Phase D: Generic route-type matcher (gRPC, GraphQL, tRPC) ──── */
/* Look up a node's qualified_name by id. Returns true if found. */
static bool lookup_node_qn(struct sqlite3 *db, int64_t node_id, char *out, size_t out_sz) {
out[0] = '\0';
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, "SELECT qualified_name FROM nodes WHERE id = ?1", CBM_NOT_FOUND, &st,
NULL) != SQLITE_OK) {
return false;
}
sqlite3_bind_int64(st, SKIP_ONE, node_id);
bool found = false;
if (sqlite3_step(st) == SQLITE_ROW) {
const char *qn = (const char *)sqlite3_column_text(st, 0);
if (qn) {
snprintf(out, out_sz, "%s", qn);
found = true;
}
}
sqlite3_finalize(st);
return found;
}
/* Match edges of a given type against Route nodes with a given QN prefix.
* Reuses the same infrastructure as HTTP/async matching. */
static int match_typed_routes(cbm_store_t *src_store, const char *src_project,
cbm_store_t *tgt_store, const char *tgt_project,
const char *edge_type, const char *svc_key, const char *method_key,
const char *cross_edge_type) {
struct sqlite3 *src_db = cbm_store_get_db(src_store);
if (!src_db) {
return 0;
}
char sql[CBM_SZ_256];
snprintf(sql, sizeof(sql),
"SELECT e.source_id, e.target_id, e.properties FROM edges e "
"WHERE e.project = ?1 AND e.type = '%s'",
edge_type);
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(src_db, sql, CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
int count = 0;
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
int64_t caller_id = sqlite3_column_int64(s, 0);
int64_t route_id = sqlite3_column_int64(s, SKIP_ONE);
const char *props = (const char *)sqlite3_column_text(s, PAIR_LEN);
char svc_val[CBM_SZ_256] = {0};
char meth_val[CBM_SZ_256] = {0};
json_str_prop(props, svc_key, svc_val, sizeof(svc_val));
json_str_prop(props, method_key, meth_val, sizeof(meth_val));
if (!svc_val[0] && !meth_val[0]) {
continue;
}
/* Look up the Route QN from the target node (already points to the Route). */
char route_qn[CR_QN_BUF] = {0};
if (!lookup_node_qn(src_db, route_id, route_qn, sizeof(route_qn))) {
continue;
}
char handler_name[CBM_SZ_256] = {0};
char handler_file[CBM_SZ_512] = {0};
int64_t handler_id =
find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
handler_file, sizeof(handler_file));
if (handler_id == 0) {
continue;
}
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
tgt_store, tgt_project, handler_id, route_qn, handler_name,
handler_file, svc_val, svc_key, cross_edge_type);
count++;
}
sqlite3_finalize(s);
return count;
}
/* ── Collect target projects ─────────────────────────────────────── */
/* 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
result.http_edges += match_http_routes(src_store, project, tgt_store, tgt);
result.async_edges += match_async_routes(src_store, project, tgt_store, tgt);
result.channel_edges += match_channels(src_store, project, tgt_store, tgt);
result.grpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "GRPC_CALLS",
"service", "method", "CROSS_GRPC_CALLS");
result.graphql_edges +=
match_typed_routes(src_store, project, tgt_store, tgt, "GRAPHQL_CALLS", "operation",
"operation", "CROSS_GRAPHQL_CALLS");
result.trpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "TRPC_CALLS",
"procedure", "procedure", "CROSS_TRPC_CALLS");
result.projects_scanned++;
cbm_store_close(tgt_store);
@@ -582,10 +676,9 @@ cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **t
result.elapsed_ms = ((double)(t1.tv_sec - t0.tv_sec) * CR_MS_PER_SEC) +
((double)(t1.tv_nsec - t0.tv_nsec) / CR_NS_PER_MS);
int total = result.http_edges + result.async_edges + result.channel_edges;
cbm_log_info("cross_repo.done", "project", project, "http", cr_itoa(result.http_edges), "async",
cr_itoa(result.async_edges), "channel", cr_itoa(result.channel_edges), "total",
cr_itoa(total));
int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
result.graphql_edges + result.trpc_edges;
cbm_log_info("cross_repo.done", "project", project, "total", cr_itoa(total));
return result;
}
+3
View File
@@ -12,6 +12,9 @@ typedef struct {
int http_edges; /* CROSS_HTTP_CALLS edges created */
int async_edges; /* CROSS_ASYNC_CALLS edges created */
int channel_edges; /* CROSS_CHANNEL edges created */
int grpc_edges; /* CROSS_GRPC_CALLS edges created */
int graphql_edges; /* CROSS_GRAPHQL_CALLS edges created */
int trpc_edges; /* CROSS_TRPC_CALLS edges created */
int projects_scanned;
double elapsed_ms;
} cbm_cross_repo_result_t;
+156
View File
@@ -1141,6 +1141,156 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
}
}
/* Extract gRPC service and method from a callee name.
* Handles patterns like: pb.NewFooServiceClient(conn).GetBar → Foo/GetBar
* Also: FooServiceGrpc.newBlockingStub(ch).getBar → FooService/getBar */
static bool extract_grpc_service_method(const char *callee, char *service, size_t srv_sz,
char *method, size_t meth_sz) {
service[0] = '\0';
method[0] = '\0';
if (!callee) {
return false;
}
/* Find last dot to split service.Method */
const char *last_dot = strrchr(callee, '.');
if (!last_dot || !last_dot[SKIP_ONE]) {
return false;
}
snprintf(method, meth_sz, "%s", last_dot + SKIP_ONE);
/* Extract service name: everything before the last dot, stripped of prefixes/suffixes */
size_t prefix_len = (size_t)(last_dot - callee);
char raw[CBM_SZ_256];
if (prefix_len >= sizeof(raw)) {
prefix_len = sizeof(raw) - SKIP_ONE;
}
memcpy(raw, callee, prefix_len);
raw[prefix_len] = '\0';
/* Strip common prefixes: pb.New, New, pb. */
const char *s = raw;
if (strncmp(s, "pb.New", CBM_SZ_6) == 0) {
s += CBM_SZ_6;
} else if (strncmp(s, "pb.", CBM_SZ_3) == 0 || strncmp(s, "New", CBM_SZ_3) == 0) {
s += CBM_SZ_3;
}
/* Strip common suffixes: Client, ServiceClient, ServiceGrpc, Stub */
snprintf(service, srv_sz, "%s", s);
size_t slen = strlen(service);
static const char *suffixes[] = {"ServiceClient", "Client", "ServiceGrpc", "BlockingStub",
"FutureStub", "Stub", "Servicer", NULL};
for (const char **sfx = suffixes; *sfx; sfx++) {
size_t flen = strlen(*sfx);
if (slen > flen && strcmp(service + slen - flen, *sfx) == 0) {
service[slen - flen] = '\0';
break;
}
}
return service[0] && method[0];
}
/* Emit GRPC_CALLS edge via gRPC Route node. */
static void emit_grpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
char service[CBM_SZ_256];
char method[CBM_SZ_256];
if (!extract_grpc_service_method(call->callee_name, service, sizeof(service), method,
sizeof(method))) {
return;
}
char route_qn[CBM_SZ_512];
snprintf(route_qn, sizeof(route_qn), "__grpc__%s/%s", service, method);
char route_name[CBM_SZ_256];
snprintf(route_name, sizeof(route_name), "%s/%s", service, method);
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", route_name, route_qn, "", 0, 0,
"{\"source\":\"grpc\"}");
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props),
"{\"callee\":\"%s\",\"service\":\"%s\",\"method\":\"%s\",\"confidence\":%.2f}", esc_c,
service, method, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "GRPC_CALLS", props);
}
/* Emit GRAPHQL_CALLS edge. Extract operation from first string arg if available. */
static void emit_graphql_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
const char *op = call->first_string_arg;
if (!op || !op[0]) {
op = call->callee_name;
}
/* Try to extract a query/mutation name from the operation string */
char op_name[CBM_SZ_256];
snprintf(op_name, sizeof(op_name), "%s", op);
/* Trim leading whitespace and "query "/"mutation " prefix */
const char *p = op_name;
while (*p == ' ' || *p == '\t' || *p == '\n') {
p++;
}
if (strncmp(p, "query ", CBM_SZ_6) == 0) {
p += CBM_SZ_6;
} else if (strncmp(p, "mutation ", CBM_SZ_8) == 0) {
p += CBM_SZ_8;
}
char route_qn[CBM_SZ_512];
snprintf(route_qn, sizeof(route_qn), "__graphql__%s", p);
int64_t route_id =
cbm_gbuf_upsert_node(gbuf, "Route", p, route_qn, "", 0, 0, "{\"source\":\"graphql\"}");
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"operation\":\"%s\",\"confidence\":%.2f}",
esc_c, p, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "GRAPHQL_CALLS", props);
}
/* Emit TRPC_CALLS edge. Extract procedure path from callee chain. */
static void emit_trpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_resolution_t *res) {
/* tRPC calls: trpc.user.getById.query() → extract "user.getById" */
const char *callee = call->callee_name;
if (!callee) {
return;
}
/* Strip trailing .query/.mutate/.subscribe */
char proc[CBM_SZ_256];
snprintf(proc, sizeof(proc), "%s", callee);
char *last_dot = strrchr(proc, '.');
if (last_dot && (strcmp(last_dot, ".query") == 0 || strcmp(last_dot, ".mutate") == 0 ||
strcmp(last_dot, ".subscribe") == 0 || strcmp(last_dot, ".useQuery") == 0 ||
strcmp(last_dot, ".useMutation") == 0)) {
*last_dot = '\0';
}
/* Strip leading trpc. */
const char *p = proc;
if (strncmp(p, "trpc.", CBM_SZ_5) == 0) {
p += CBM_SZ_5;
}
char route_qn[CBM_SZ_512];
snprintf(route_qn, sizeof(route_qn), "__trpc__%s", p);
int64_t route_id =
cbm_gbuf_upsert_node(gbuf, "Route", p, route_qn, "", 0, 0, "{\"source\":\"trpc\"}");
char esc_c[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
char props[CBM_SZ_1K];
snprintf(props, sizeof(props), "{\"callee\":\"%s\",\"procedure\":\"%s\",\"confidence\":%.2f}",
esc_c, p, res->confidence);
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "TRPC_CALLS", props);
}
static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const cbm_gbuf_node_t *target, const CBMCall *call,
const cbm_resolution_t *res, const char *module_qn,
@@ -1171,6 +1321,12 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
if ((svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) && (has_url || has_topic)) {
emit_http_async_service_edge(gbuf, source, call, res, svc, arg);
} else if (svc == CBM_SVC_GRPC) {
emit_grpc_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_GRAPHQL) {
emit_graphql_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_TRPC) {
emit_trpc_edge(gbuf, source, call, res);
} else if (svc == CBM_SVC_CONFIG) {
emit_config_edge(gbuf, source, target, call, res, arg);
} else {
+73
View File
@@ -685,6 +685,74 @@ static void create_route_data_flows(cbm_gbuf_t *gb, const cbm_gbuf_node_t *route
}
}
/* Find the enclosing proto service for a function by file + line range. */
static const cbm_gbuf_node_t *find_enclosing_service(const cbm_gbuf_node_t *fn,
const cbm_gbuf_node_t **services,
int svc_count) {
for (int s = 0; s < svc_count; s++) {
if (strcmp(fn->file_path, services[s]->file_path) != 0) {
continue;
}
if (fn->start_line >= services[s]->start_line && fn->end_line <= services[s]->end_line) {
return services[s];
}
}
return NULL;
}
/* Phase 4: Create __grpc__ Route nodes from protobuf service definitions. */
static void create_grpc_routes(cbm_gbuf_t *gb) {
const cbm_gbuf_node_t **classes = NULL;
int class_count = 0;
if (cbm_gbuf_find_by_label(gb, "Class", &classes, &class_count) != 0 || class_count == 0) {
return;
}
const cbm_gbuf_node_t *services[CBM_SZ_64];
int svc_count = 0;
for (int i = 0; i < class_count && svc_count < CBM_SZ_64; i++) {
if (classes[i]->file_path && strstr(classes[i]->file_path, ".proto")) {
services[svc_count++] = classes[i];
}
}
if (svc_count == 0) {
return;
}
const cbm_gbuf_node_t **funcs = NULL;
int func_count = 0;
if (cbm_gbuf_find_by_label(gb, "Function", &funcs, &func_count) != 0 || func_count == 0) {
return;
}
int grpc_routes = 0;
for (int f = 0; f < func_count; f++) {
const cbm_gbuf_node_t *fn = funcs[f];
if (!fn->file_path || !strstr(fn->file_path, ".proto") || !fn->name) {
continue;
}
const cbm_gbuf_node_t *svc = find_enclosing_service(fn, services, svc_count);
if (!svc) {
continue;
}
char route_qn[CBM_ROUTE_QN_SIZE];
snprintf(route_qn, sizeof(route_qn), "__grpc__%s/%s", svc->name, fn->name);
char props[CBM_SZ_128];
snprintf(props, sizeof(props), "{\"source\":\"proto\",\"service\":\"%s\"}", svc->name);
int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", fn->name, route_qn, fn->file_path,
fn->start_line, fn->end_line, props);
cbm_gbuf_insert_edge(gb, fn->id, route_id, "HANDLES", "{\"via\":\"proto_rpc\"}");
grpc_routes++;
}
if (grpc_routes > 0) {
char buf[CBM_SZ_16];
snprintf(buf, sizeof(buf), "%d", grpc_routes);
cbm_log_info("pass.route_nodes.grpc", "routes", buf);
}
}
/* Phase 3: Create DATA_FLOWS edges by linking callers through Route to handlers. */
static void create_data_flows(cbm_gbuf_t *gb) {
const cbm_gbuf_node_t **routes = NULL;
@@ -739,4 +807,9 @@ void cbm_pipeline_create_route_nodes(cbm_gbuf_t *gb) {
/* Phase 3: create DATA_FLOWS edges through Routes */
create_data_flows(gb);
/* Phase 4: create gRPC Route nodes from protobuf service definitions.
* Scans Class nodes from .proto files, follows DEFINES_METHOD edges
* to find rpc methods, creates __grpc__ServiceName/MethodName Route nodes. */
create_grpc_routes(gb);
}