Skip to content

Commit e7893a2

Browse files
author
test
committed
Add gRPC stub detection in call resolution and chained call extraction
- pass_parallel.c: detect gRPC calls when resolved QN contains ServiceClient/ServiceGrpc/Servicer — emits GRPC_CALLS edges for calls resolved to generated protobuf client interfaces - pass_parallel.c: emit_grpc_edge falls back to resolved QN when callee_name alone doesn't contain service.method pattern - extract_calls.c: add resolve_chained_selector() for iterative extraction of chained method calls (NewClient(conn).GetBar → "NewClient.GetBar") without recursion Known limitation: Go receiver-method chained calls like pb.NewCartServiceClient(conn).GetCart() in rpc.go are not yet extracted as CALLS edges — the Go grammar's AST structure for these patterns needs dedicated investigation.
1 parent 2343f8e commit e7893a2

2 files changed

Lines changed: 74 additions & 6 deletions

File tree

internal/cbm/extract_calls.c

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,65 @@ static bool lean_is_in_type_position(TSNode node) {
101101
return false;
102102
}
103103

104+
/* Resolve a selector_expression that may chain through call_expressions.
105+
* Go pattern: pb.NewFooClient(conn).GetBar → "pb.NewFooClient.GetBar"
106+
* Without this, cbm_node_text returns full text including args/parens.
107+
* Iteratively walks the chain: selector → operand(call) → function(selector) → ... */
108+
static char *resolve_chained_selector(CBMArena *a, TSNode sel, const char *source) {
109+
TSNode operand = ts_node_child_by_field_name(sel, TS_FIELD("operand"));
110+
TSNode field = ts_node_child_by_field_name(sel, TS_FIELD("field"));
111+
if (ts_node_is_null(operand) || ts_node_is_null(field) ||
112+
strcmp(ts_node_type(operand), "call_expression") != 0) {
113+
return cbm_node_text(a, sel, source);
114+
}
115+
116+
/* Operand is a call_expression — extract its callee iteratively.
117+
* Walk: call_expression → function field → if selector_expression, repeat. */
118+
char *method = cbm_node_text(a, field, source);
119+
TSNode inner = operand;
120+
enum { MAX_CHAIN_DEPTH = 4 };
121+
for (int depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
122+
TSNode fn = ts_node_child_by_field_name(inner, TS_FIELD("function"));
123+
if (ts_node_is_null(fn)) {
124+
break;
125+
}
126+
const char *fnk = ts_node_type(fn);
127+
if (strcmp(fnk, "selector_expression") == 0) {
128+
/* Check if this selector also chains through a call */
129+
TSNode inner_op = ts_node_child_by_field_name(fn, TS_FIELD("operand"));
130+
if (!ts_node_is_null(inner_op) &&
131+
strcmp(ts_node_type(inner_op), "call_expression") == 0) {
132+
inner = inner_op;
133+
continue;
134+
}
135+
}
136+
/* Reached a non-chained callee — extract its text */
137+
char *base = cbm_node_text(a, fn, source);
138+
if (base && method) {
139+
return cbm_arena_sprintf(a, "%s.%s", base, method);
140+
}
141+
return method;
142+
}
143+
144+
/* Fallback: just return the method name */
145+
return method;
146+
}
147+
104148
// Try common field-based callee resolution (function, name, method fields).
105149
static char *extract_callee_from_fields(CBMArena *a, TSNode node, const char *source) {
106150
// Try "function" field
107151
TSNode func_node = ts_node_child_by_field_name(node, TS_FIELD("function"));
108152
if (!ts_node_is_null(func_node)) {
109153
const char *fk = ts_node_type(func_node);
154+
if (strcmp(fk, "selector_expression") == 0) {
155+
return resolve_chained_selector(a, func_node, source);
156+
}
110157
if (strcmp(fk, "identifier") == 0 || strcmp(fk, "simple_identifier") == 0 ||
111-
strcmp(fk, "selector_expression") == 0 || strcmp(fk, "attribute") == 0 ||
112-
strcmp(fk, "member_expression") == 0 || strcmp(fk, "field_expression") == 0 ||
113-
strcmp(fk, "dot") == 0 || strcmp(fk, "function") == 0 ||
114-
strcmp(fk, "dotted_identifier") == 0 || strcmp(fk, "member_access_expression") == 0 ||
115-
strcmp(fk, "scoped_identifier") == 0 || strcmp(fk, "qualified_identifier") == 0) {
158+
strcmp(fk, "attribute") == 0 || strcmp(fk, "member_expression") == 0 ||
159+
strcmp(fk, "field_expression") == 0 || strcmp(fk, "dot") == 0 ||
160+
strcmp(fk, "function") == 0 || strcmp(fk, "dotted_identifier") == 0 ||
161+
strcmp(fk, "member_access_expression") == 0 || strcmp(fk, "scoped_identifier") == 0 ||
162+
strcmp(fk, "qualified_identifier") == 0) {
116163
return cbm_node_text(a, func_node, source);
117164
}
118165
}

src/pipeline/pass_parallel.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1217,9 +1217,18 @@ static void emit_grpc_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, cons
12171217
const cbm_resolution_t *res) {
12181218
char service[CBM_SZ_256];
12191219
char method[CBM_SZ_256];
1220+
/* Try callee_name first (e.g., "pb.NewCartServiceClient.GetCart") */
12201221
if (!extract_grpc_service_method(call->callee_name, service, sizeof(service), method,
12211222
sizeof(method))) {
1222-
return;
1223+
/* Fallback: try the resolved QN for Go chained calls.
1224+
* Go pattern: pb.NewCartServiceClient(conn).GetCart(ctx, req)
1225+
* callee_name = "GetCart", QN = "...CartServiceClient.GetCart"
1226+
* The QN contains the full ServiceClient.Method pattern. */
1227+
if (!res->qualified_name ||
1228+
!extract_grpc_service_method(res->qualified_name, service, sizeof(service), method,
1229+
sizeof(method))) {
1230+
return;
1231+
}
12231232
}
12241233

12251234
char route_qn[CBM_SZ_512];
@@ -1326,6 +1335,18 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
13261335
svc = CBM_SVC_ROUTE_REG;
13271336
}
13281337

1338+
/* Detect gRPC stub method calls by resolved QN.
1339+
* Go pattern: pb.NewCartServiceClient(conn).GetCart(ctx, req)
1340+
* Tree-sitter extracts GetCart as the callee, which resolves to the
1341+
* generated pb interface method (QN contains "ServiceClient"). */
1342+
if (svc == CBM_SVC_NONE && res->qualified_name) {
1343+
if (strstr(res->qualified_name, "ServiceClient") != NULL ||
1344+
strstr(res->qualified_name, "ServiceGrpc") != NULL ||
1345+
strstr(res->qualified_name, "Servicer") != NULL) {
1346+
svc = CBM_SVC_GRPC;
1347+
}
1348+
}
1349+
13291350
if (svc == CBM_SVC_ROUTE_REG) {
13301351
const char *handler_ref = NULL;
13311352
const char *route_path = find_route_path_in_args(call, &handler_ref);

0 commit comments

Comments
 (0)