mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
b6eefe014c
commit
81385d08eb
9 files changed
+488
-23
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user