Skip to content

Commit 8bf489e

Browse files
authored
Merge pull request #2905 from divakantpandey/fix/2723-retrofit-client-http-calls
fix(extract): emit HTTP_CALLS for Retrofit interface methods (#2723)
2 parents 1cee123 + 49d6b46 commit 8bf489e

9 files changed

Lines changed: 374 additions & 10 deletions

File tree

‎internal/cbm/cbm.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ typedef struct {
241241
const char **return_types; // NULL-terminated array (NULL if none)
242242
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
243243
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
244+
// Path a client interface method requests (Retrofit @GET("v1/x")) or NULL;
245+
// route_method is then its verb.
246+
const char *client_route_path;
244247
int complexity; // cyclomatic complexity
245248
int cognitive; // cognitive complexity (nesting-weighted)
246249
int loop_count; // number of loop constructs in the body

‎internal/cbm/extract_defs.c‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3602,6 +3602,105 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c
36023602
extract_route_from_annotations(a, func_node, source, spec, out_path, out_method);
36033603
}
36043604

3605+
/* The string literal an annotation argument consists of, positional or as
3606+
* `value = "..."`. A concatenation, a constant or a template has no statically
3607+
* known value, so it yields a null node rather than one of its fragments. */
3608+
static TSNode annotation_arg_literal(TSNode arg) {
3609+
TSNode null_node = {0};
3610+
const char *kind = ts_node_type(arg);
3611+
if (strcmp(kind, "element_value_pair") == 0 || strcmp(kind, "value_argument") == 0) {
3612+
uint32_t nc = ts_node_named_child_count(arg);
3613+
if (nc == 0) {
3614+
return null_node;
3615+
}
3616+
arg = ts_node_named_child(arg, nc - SKIP_ONE);
3617+
}
3618+
if (!is_route_string_kind(ts_node_type(arg))) {
3619+
return null_node;
3620+
}
3621+
for (uint32_t i = 0; i < ts_node_named_child_count(arg); i++) {
3622+
const char *ck = ts_node_type(ts_node_named_child(arg, i));
3623+
if (strcmp(ck, "interpolated_identifier") == 0 ||
3624+
strcmp(ck, "interpolated_expression") == 0 || strcmp(ck, "string_interpolation") == 0) {
3625+
return null_node;
3626+
}
3627+
}
3628+
return arg;
3629+
}
3630+
3631+
/* Java interface_declaration, or a Kotlin class_declaration spelled with the
3632+
* `interface` keyword (the grammar has no separate node kind for it). */
3633+
static bool is_jvm_interface(TSNode class_node) {
3634+
if (ts_node_is_null(class_node)) {
3635+
return false;
3636+
}
3637+
const char *kind = ts_node_type(class_node);
3638+
if (strcmp(kind, "interface_declaration") == 0) {
3639+
return true;
3640+
}
3641+
if (strcmp(kind, "class_declaration") != 0) {
3642+
return false;
3643+
}
3644+
for (uint32_t i = 0; i < ts_node_child_count(class_node); i++) {
3645+
TSNode ch = ts_node_child(class_node, i);
3646+
if (!ts_node_is_named(ch) && strcmp(ts_node_type(ch), "interface") == 0) {
3647+
return true;
3648+
}
3649+
}
3650+
return false;
3651+
}
3652+
3653+
/* Retrofit declares a client's requests as @GET("v1/orders") / @POST(...) on an
3654+
* interface method. JAX-RS uses the same bare verb names, but its verbs are
3655+
* marker annotations (the path is a sibling @Path), so a verb that carries an
3656+
* argument is a Retrofit request, not a served endpoint. Returns true for such
3657+
* a method; *out_path is its path, or NULL when the argument is not a literal.
3658+
* Retrofit resolves a relative path against the base URL, so it is rooted the
3659+
* way JAX-RS paths are; a full URL is kept as written. */
3660+
static bool retrofit_client_route(CBMArena *a, TSNode func_node, const char *source,
3661+
const CBMLangSpec *spec, const char **out_path,
3662+
const char **out_method) {
3663+
*out_path = NULL;
3664+
*out_method = NULL;
3665+
TSNode wrappers[MAX_ATTR_WRAPPERS];
3666+
int wn = find_jvm_modifiers(func_node, spec->language, wrappers, MAX_ATTR_WRAPPERS_MINUS_1);
3667+
wrappers[wn++] = func_node;
3668+
for (int w = 0; w < wn; w++) {
3669+
uint32_t cc = ts_node_child_count(wrappers[w]);
3670+
for (uint32_t ci = 0; ci < cc; ci++) {
3671+
TSNode child = ts_node_child(wrappers[w], ci);
3672+
if (!cbm_kind_in_set(child, spec->decorator_node_types)) {
3673+
continue;
3674+
}
3675+
TSNode name_node = annotation_name_node(child);
3676+
if (ts_node_is_null(name_node)) {
3677+
continue;
3678+
}
3679+
char *name = cbm_node_text(a, name_node, source);
3680+
const char *method = annotation_route_method(name);
3681+
if (!method || strcmp(method, name) != 0) {
3682+
continue;
3683+
}
3684+
TSNode args = annotation_args_node(child);
3685+
if (ts_node_is_null(args) || ts_node_named_child_count(args) == 0) {
3686+
continue;
3687+
}
3688+
*out_method = method;
3689+
TSNode lit = annotation_arg_literal(ts_node_named_child(args, 0));
3690+
char *text = ts_node_is_null(lit) ? NULL : cbm_node_text(a, lit, source);
3691+
size_t len = text ? strlen(text) : 0;
3692+
if (len > PAIR_CHARS && (text[0] == '"' || text[0] == '\'')) {
3693+
char *path = cbm_arena_strndup(a, text + SKIP_CHAR, len - PAIR_CHARS);
3694+
*out_path = (!path || path[0] == '/' || strstr(path, "://"))
3695+
? path
3696+
: cbm_arena_sprintf(a, "/%s", path);
3697+
}
3698+
return true;
3699+
}
3700+
}
3701+
return false;
3702+
}
3703+
36053704
static const char *join_route_paths(CBMArena *a, const char *prefix, const char *path) {
36063705
if (!path || !path[0]) {
36073706
return prefix;
@@ -9321,6 +9420,17 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
93219420

93229421
def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec);
93239422
extract_route_from_decorators(a, child, ctx->source, spec, &def.route_path, &def.route_method);
9423+
const char *client_path = NULL;
9424+
const char *client_method = NULL;
9425+
/* Retrofit services must be interfaces; Pippo and Rapidoid put the path in
9426+
* @GET("/x") on a controller class, and those methods serve the route. */
9427+
if ((ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN) &&
9428+
is_jvm_interface(class_node) &&
9429+
retrofit_client_route(a, child, ctx->source, spec, &client_path, &client_method)) {
9430+
def.route_path = NULL;
9431+
def.client_route_path = client_path;
9432+
def.route_method = client_path ? client_method : NULL;
9433+
}
93249434
if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN ||
93259435
ctx->language == CBM_LANG_SCALA)) {
93269436
const char *prefix = spring_class_route_prefix(a, class_node, ctx->source, spec);

‎internal/cbm/result_compact.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
268268
cr_list(c, &d->return_types);
269269
cr_str(c, &d->route_path);
270270
cr_str(c, &d->route_method);
271+
cr_str(c, &d->client_route_path);
271272
cr_blob(c, (const void **)&d->fingerprint,
272273
d->fingerprint_k > 0 ? (size_t)d->fingerprint_k * sizeof(uint32_t) : 0);
273274
cr_str(c, &d->structural_profile);

‎src/pipeline/pass_definitions.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
322322
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
323323
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
324324
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
325+
append_json_string(buf, bufsize, &pos, "client_route_path", def->client_route_path);
325326
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
326327
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
327328
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
@@ -402,6 +403,7 @@ static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
402403
pd_arr_field_len("param_types", def->param_types) +
403404
pd_str_field_len("route_path", def->route_path) +
404405
pd_str_field_len("route_method", def->route_method) +
406+
pd_str_field_len("client_route_path", def->client_route_path) +
405407
pd_str_field_len("http_client", def->http_client) +
406408
pd_str_field_len("http_base_url", def->http_base_url);
407409
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {

‎src/pipeline/pass_parallel.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
552552
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
553553
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
554554
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
555+
append_json_string(buf, bufsize, &pos, "client_route_path", def->client_route_path);
555556
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
556557
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
557558
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
@@ -780,6 +781,7 @@ static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
780781
pp_arr_field_len("param_types", def->param_types) +
781782
pp_str_field_len("route_path", def->route_path) +
782783
pp_str_field_len("route_method", def->route_method) +
784+
pp_str_field_len("client_route_path", def->client_route_path) +
783785
pp_str_field_len("http_client", def->http_client) +
784786
pp_str_field_len("http_base_url", def->http_base_url);
785787
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {

‎src/pipeline/pass_route_nodes.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,36 @@ static void match_infra_routes(cbm_gbuf_t *gb) {
397397
}
398398
}
399399

400+
/* A client interface method's declared request (Retrofit): an HTTP_CALLS edge
401+
* to the Route, with the url_path/method properties a call site's edge carries,
402+
* so the endpoint is matched to its server like any other outbound call. */
403+
static void emit_client_route_call(cbm_gbuf_t *gb, int64_t func_id, const char *path,
404+
const char *method) {
405+
const char *rm = method ? method : "ANY";
406+
char route_qn[CBM_ROUTE_QN_SIZE];
407+
char cpath[CBM_SZ_256];
408+
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm,
409+
cbm_route_canon_path(path, cpath, sizeof(cpath)));
410+
char esc_m[CBM_SZ_128];
411+
cbm_json_escape(esc_m, sizeof(esc_m), rm);
412+
char rprops[CBM_SZ_256];
413+
(void)snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\"}", esc_m);
414+
int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", path, route_qn, "", 0, 0, rprops);
415+
char esc_p[CBM_SZ_512];
416+
cbm_json_escape(esc_p, sizeof(esc_p), path);
417+
char eprops[CBM_SZ_1K];
418+
(void)snprintf(eprops, sizeof(eprops),
419+
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"method\":\"%s\"}", esc_m, esc_p,
420+
esc_m);
421+
cbm_gbuf_insert_edge(gb, func_id, route_id, "HTTP_CALLS", eprops);
422+
}
423+
400424
void cbm_pipeline_emit_def_route(cbm_gbuf_t *gb, int64_t func_id, const CBMDefinition *def,
401425
const char *rel) {
426+
if (def->client_route_path && def->client_route_path[0]) {
427+
emit_client_route_call(gb, func_id, def->client_route_path, def->route_method);
428+
return;
429+
}
402430
if (!def->route_path || def->route_path[0] == '\0') {
403431
return;
404432
}

‎src/pipeline/pipeline.c‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2604,9 +2604,10 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
26042604
}
26052605
str_def_fp += def->fingerprint ? (size_t)def->fingerprint_k * sizeof(uint32_t) : 0;
26062606
str_def_misc += census_len(def->route_path) + census_len(def->route_method) +
2607-
census_len(def->impl_trait) + census_len(def->http_base_url) +
2608-
census_list(def->extra_props) + census_len(def->decl_namespace) +
2609-
census_len(def->variant_key) + census_len(def->alias_target);
2607+
census_len(def->client_route_path) + census_len(def->impl_trait) +
2608+
census_len(def->http_base_url) + census_list(def->extra_props) +
2609+
census_len(def->decl_namespace) + census_len(def->variant_key) +
2610+
census_len(def->alias_target);
26102611
}
26112612
for (int c = 0; c < r->calls.count; c++) {
26122613
const CBMCall *call = &r->calls.items[c];

0 commit comments

Comments
 (0)