diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 5da9013b7..b40d9f551 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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; } diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 426db947b..5701ab4ac 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -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}, diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index eebaeda0d..a723a84dd 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -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; } diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index b526a8f7f..6f042123e 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -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. */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 0430bdfbc..e75860593 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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); diff --git a/src/pipeline/pass_cross_repo.c b/src/pipeline/pass_cross_repo.c index 57b2e5273..07f5ca7e3 100644 --- a/src/pipeline/pass_cross_repo.c +++ b/src/pipeline/pass_cross_repo.c @@ -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; } diff --git a/src/pipeline/pass_cross_repo.h b/src/pipeline/pass_cross_repo.h index ccd4f8b1c..5d2d4cfee 100644 --- a/src/pipeline/pass_cross_repo.h +++ b/src/pipeline/pass_cross_repo.h @@ -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; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 76a2c0e65..4a410a35f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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 { diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 12262cb00..0747447fd 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -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); }