Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ typedef struct {
const char **return_types; // NULL-terminated array (NULL if none)
const char *route_path; // HTTP route path from decorator (e.g., "/api/users") or NULL
const char *route_method; // HTTP method from decorator (e.g., "POST") or NULL
// Path a client interface method requests (Retrofit @GET("v1/x")) or NULL;
// route_method is then its verb.
const char *client_route_path;
int complexity; // cyclomatic complexity
int cognitive; // cognitive complexity (nesting-weighted)
int loop_count; // number of loop constructs in the body
Expand Down
110 changes: 110 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3602,6 +3602,105 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c
extract_route_from_annotations(a, func_node, source, spec, out_path, out_method);
}

/* The string literal an annotation argument consists of, positional or as
* `value = "..."`. A concatenation, a constant or a template has no statically
* known value, so it yields a null node rather than one of its fragments. */
static TSNode annotation_arg_literal(TSNode arg) {
TSNode null_node = {0};
const char *kind = ts_node_type(arg);
if (strcmp(kind, "element_value_pair") == 0 || strcmp(kind, "value_argument") == 0) {
uint32_t nc = ts_node_named_child_count(arg);
if (nc == 0) {
return null_node;
}
arg = ts_node_named_child(arg, nc - SKIP_ONE);
}
if (!is_route_string_kind(ts_node_type(arg))) {
return null_node;
}
for (uint32_t i = 0; i < ts_node_named_child_count(arg); i++) {
const char *ck = ts_node_type(ts_node_named_child(arg, i));
if (strcmp(ck, "interpolated_identifier") == 0 ||
strcmp(ck, "interpolated_expression") == 0 || strcmp(ck, "string_interpolation") == 0) {
return null_node;
}
}
return arg;
}

/* Java interface_declaration, or a Kotlin class_declaration spelled with the
* `interface` keyword (the grammar has no separate node kind for it). */
static bool is_jvm_interface(TSNode class_node) {
if (ts_node_is_null(class_node)) {
return false;
}
const char *kind = ts_node_type(class_node);
if (strcmp(kind, "interface_declaration") == 0) {
return true;
}
if (strcmp(kind, "class_declaration") != 0) {
return false;
}
for (uint32_t i = 0; i < ts_node_child_count(class_node); i++) {
TSNode ch = ts_node_child(class_node, i);
if (!ts_node_is_named(ch) && strcmp(ts_node_type(ch), "interface") == 0) {
return true;
}
}
return false;
}

/* Retrofit declares a client's requests as @GET("v1/orders") / @POST(...) on an
* interface method. JAX-RS uses the same bare verb names, but its verbs are
* marker annotations (the path is a sibling @Path), so a verb that carries an
* argument is a Retrofit request, not a served endpoint. Returns true for such
* a method; *out_path is its path, or NULL when the argument is not a literal.
* Retrofit resolves a relative path against the base URL, so it is rooted the
* way JAX-RS paths are; a full URL is kept as written. */
static bool retrofit_client_route(CBMArena *a, TSNode func_node, const char *source,
const CBMLangSpec *spec, const char **out_path,
const char **out_method) {
*out_path = NULL;
*out_method = NULL;
TSNode wrappers[MAX_ATTR_WRAPPERS];
int wn = find_jvm_modifiers(func_node, spec->language, wrappers, MAX_ATTR_WRAPPERS_MINUS_1);
wrappers[wn++] = func_node;
for (int w = 0; w < wn; w++) {
uint32_t cc = ts_node_child_count(wrappers[w]);
for (uint32_t ci = 0; ci < cc; ci++) {
TSNode child = ts_node_child(wrappers[w], ci);
if (!cbm_kind_in_set(child, spec->decorator_node_types)) {
continue;
}
TSNode name_node = annotation_name_node(child);
if (ts_node_is_null(name_node)) {
continue;
}
char *name = cbm_node_text(a, name_node, source);
const char *method = annotation_route_method(name);
if (!method || strcmp(method, name) != 0) {
continue;
}
TSNode args = annotation_args_node(child);
if (ts_node_is_null(args) || ts_node_named_child_count(args) == 0) {
continue;
}
*out_method = method;
TSNode lit = annotation_arg_literal(ts_node_named_child(args, 0));
char *text = ts_node_is_null(lit) ? NULL : cbm_node_text(a, lit, source);
size_t len = text ? strlen(text) : 0;
if (len > PAIR_CHARS && (text[0] == '"' || text[0] == '\'')) {
char *path = cbm_arena_strndup(a, text + SKIP_CHAR, len - PAIR_CHARS);
*out_path = (!path || path[0] == '/' || strstr(path, "://"))
? path
: cbm_arena_sprintf(a, "/%s", path);
}
return true;
}
}
return false;
}

static const char *join_route_paths(CBMArena *a, const char *prefix, const char *path) {
if (!path || !path[0]) {
return prefix;
Expand Down Expand Up @@ -9321,6 +9420,17 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,

def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec);
extract_route_from_decorators(a, child, ctx->source, spec, &def.route_path, &def.route_method);
const char *client_path = NULL;
const char *client_method = NULL;
/* Retrofit services must be interfaces; Pippo and Rapidoid put the path in
* @GET("/x") on a controller class, and those methods serve the route. */
if ((ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN) &&
is_jvm_interface(class_node) &&
retrofit_client_route(a, child, ctx->source, spec, &client_path, &client_method)) {
def.route_path = NULL;
def.client_route_path = client_path;
def.route_method = client_path ? client_method : NULL;
}
if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN ||
ctx->language == CBM_LANG_SCALA)) {
const char *prefix = spring_class_route_prefix(a, class_node, ctx->source, spec);
Expand Down
1 change: 1 addition & 0 deletions internal/cbm/result_compact.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_list(c, &d->return_types);
cr_str(c, &d->route_path);
cr_str(c, &d->route_method);
cr_str(c, &d->client_route_path);
cr_blob(c, (const void **)&d->fingerprint,
d->fingerprint_k > 0 ? (size_t)d->fingerprint_k * sizeof(uint32_t) : 0);
cr_str(c, &d->structural_profile);
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "client_route_path", def->client_route_path);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
Expand Down Expand Up @@ -402,6 +403,7 @@ static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
pd_arr_field_len("param_types", def->param_types) +
pd_str_field_len("route_path", def->route_path) +
pd_str_field_len("route_method", def->route_method) +
pd_str_field_len("client_route_path", def->client_route_path) +
pd_str_field_len("http_client", def->http_client) +
pd_str_field_len("http_base_url", def->http_base_url);
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "client_route_path", def->client_route_path);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
Expand Down Expand Up @@ -780,6 +781,7 @@ static char *pp_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
pp_arr_field_len("param_types", def->param_types) +
pp_str_field_len("route_path", def->route_path) +
pp_str_field_len("route_method", def->route_method) +
pp_str_field_len("client_route_path", def->client_route_path) +
pp_str_field_len("http_client", def->http_client) +
pp_str_field_len("http_base_url", def->http_base_url);
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
Expand Down
28 changes: 28 additions & 0 deletions src/pipeline/pass_route_nodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,36 @@ static void match_infra_routes(cbm_gbuf_t *gb) {
}
}

/* A client interface method's declared request (Retrofit): an HTTP_CALLS edge
* to the Route, with the url_path/method properties a call site's edge carries,
* so the endpoint is matched to its server like any other outbound call. */
static void emit_client_route_call(cbm_gbuf_t *gb, int64_t func_id, const char *path,
const char *method) {
const char *rm = method ? method : "ANY";
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm,
cbm_route_canon_path(path, cpath, sizeof(cpath)));
char esc_m[CBM_SZ_128];
cbm_json_escape(esc_m, sizeof(esc_m), rm);
char rprops[CBM_SZ_256];
(void)snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\"}", esc_m);
int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", path, route_qn, "", 0, 0, rprops);
char esc_p[CBM_SZ_512];
cbm_json_escape(esc_p, sizeof(esc_p), path);
char eprops[CBM_SZ_1K];
(void)snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"method\":\"%s\"}", esc_m, esc_p,
esc_m);
cbm_gbuf_insert_edge(gb, func_id, route_id, "HTTP_CALLS", eprops);
}

void cbm_pipeline_emit_def_route(cbm_gbuf_t *gb, int64_t func_id, const CBMDefinition *def,
const char *rel) {
if (def->client_route_path && def->client_route_path[0]) {
emit_client_route_call(gb, func_id, def->client_route_path, def->route_method);
return;
}
if (!def->route_path || def->route_path[0] == '\0') {
return;
}
Expand Down
7 changes: 4 additions & 3 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -2604,9 +2604,10 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
}
str_def_fp += def->fingerprint ? (size_t)def->fingerprint_k * sizeof(uint32_t) : 0;
str_def_misc += census_len(def->route_path) + census_len(def->route_method) +
census_len(def->impl_trait) + census_len(def->http_base_url) +
census_list(def->extra_props) + census_len(def->decl_namespace) +
census_len(def->variant_key) + census_len(def->alias_target);
census_len(def->client_route_path) + census_len(def->impl_trait) +
census_len(def->http_base_url) + census_list(def->extra_props) +
census_len(def->decl_namespace) + census_len(def->variant_key) +
census_len(def->alias_target);
}
for (int c = 0; c < r->calls.count; c++) {
const CBMCall *call = &r->calls.items[c];
Expand Down
Loading
Loading