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
85 changes: 85 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3593,6 +3593,83 @@ 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;
}

/* 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 @@ -9306,6 +9383,14 @@ 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;
if ((ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN) &&
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 @@ -2592,9 +2592,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
125 changes: 118 additions & 7 deletions tests/test_edge_types_probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -1140,8 +1140,9 @@ static int et_qn_has_tail(const char *qn, const char *tail) {
/* route_qns (optional, NULL-terminated): Route qualified_names that must
* exist, so a test asserting that a route gets NO handler cannot pass merely
* because the route itself was never extracted. */
static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHandles *want,
int parallel, const char *const *route_qns) {
static int et_edges_exact_routes(const EtFile *files, int nfiles, const char *edge,
const EtHandles *want, int parallel,
const char *const *route_qns) {
EtProj lp;
cbm_store_t *store =
parallel ? et_index_parallel(&lp, files, nfiles) : et_index_files(&lp, files, nfiles);
Expand All @@ -1153,7 +1154,7 @@ static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHand
cbm_edge_t *edges = NULL;
int n = 0;
int ok = store != NULL &&
cbm_store_find_edges_by_type(store, lp.project, "HANDLES", &edges, &n) ==
cbm_store_find_edges_by_type(store, lp.project, edge, &edges, &n) ==
CBM_STORE_OK &&
n == wanted;
for (int i = 0; store && i < n; i++) {
Expand All @@ -1171,7 +1172,7 @@ static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHand
}
if (!matched) {
ok = 0;
fprintf(stderr, " [ET-HANDLES] unexpected %s -> %s\n",
fprintf(stderr, " [ET-%s] unexpected %s -> %s\n", edge,
have_src && src.qualified_name ? src.qualified_name : "<?>",
have_tgt && tgt.name ? tgt.name : "<?>");
}
Expand All @@ -1181,7 +1182,7 @@ static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHand
for (int wi = 0; wi < wanted; wi++) {
if (!found[wi]) {
ok = 0;
fprintf(stderr, " [ET-HANDLES] missing %s -> %s\n", want[wi].handler_tail,
fprintf(stderr, " [ET-%s] missing %s -> %s\n", edge, want[wi].handler_tail,
want[wi].route);
}
}
Expand All @@ -1190,24 +1191,131 @@ static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHand
if (!store ||
cbm_store_find_node_by_qn(store, lp.project, route_qns[ri], &route) != CBM_STORE_OK) {
ok = 0;
fprintf(stderr, " [ET-HANDLES] missing Route %s\n", route_qns[ri]);
fprintf(stderr, " [ET-%s] missing Route %s\n", edge, route_qns[ri]);
} else {
cbm_node_free_fields(&route);
}
}
if (!ok) {
fprintf(stderr, " [ET-HANDLES] FAIL (%s path) expected=%d actual=%d\n",
fprintf(stderr, " [ET-%s] FAIL (%s path) expected=%d actual=%d\n", edge,
parallel ? "parallel" : "sequential", wanted, n);
}
if (edges) cbm_store_free_edges(edges, n);
et_cleanup(&lp, store);
return ok;
}

static int et_handles_exact_routes(const EtFile *files, int nfiles, const EtHandles *want,
int parallel, const char *const *route_qns) {
return et_edges_exact_routes(files, nfiles, "HANDLES", want, parallel, route_qns);
}

static int et_handles_exact(const EtFile *files, int nfiles, const EtHandles *want, int parallel) {
return et_handles_exact_routes(files, nfiles, want, parallel, NULL);
}

/* Retrofit (#2723): an interface method's @GET("...") declares a request the
* client makes. It was minted as a served Route with a HANDLES edge, and every
* path without a leading slash collapsed onto "/". The relative path is rooted
* (Retrofit resolves it against the base URL); a path from a constant or a
* template has no static value and yields no edge rather than a "/" Route. */
static const EtFile et_retrofit_kotlin[] = {
{"src/main/kotlin/com/acme/client/OrderApi.kt",
"package com.acme.client\n\n"
"import retrofit2.http.Body\n"
"import retrofit2.http.GET\n"
"import retrofit2.http.POST\n"
"import retrofit2.http.Path\n\n"
"private const val ORDERS = \"/v1/orders\"\n\n"
"interface OrderApi {\n"
" @GET(\"/v1/orders/{id}\")\n"
" suspend fun getOrder(@Path(\"id\") id: String): String\n\n"
" @POST(\"v1/orders\")\n"
" suspend fun createOrder(@Body body: String): String\n\n"
" @GET(ORDERS)\n"
" suspend fun listOrders(): String\n\n"
" @GET(\"$ORDERS/{id}/status\")\n"
" suspend fun getOrderStatus(@Path(\"id\") id: String): String\n"
"}\n"}};

static const EtHandles et_retrofit_kotlin_calls[] = {
{"OrderApi.getOrder", "/v1/orders/{id}"}, {"OrderApi.createOrder", "/v1/orders"}, {NULL, NULL}};

static const EtHandles et_no_edges[] = {{NULL, NULL}};

TEST(http_calls_retrofit_kotlin_interface_issue2723) {
static const char *const qns[] = {"__route__GET__/v1/orders/{}", "__route__POST__/v1/orders",
NULL};
for (int parallel = 0; parallel <= 1; parallel++) {
ASSERT_TRUE(et_edges_exact_routes(et_retrofit_kotlin, 1, "HTTP_CALLS",
et_retrofit_kotlin_calls, parallel, qns));
ASSERT_TRUE(et_handles_exact(et_retrofit_kotlin, 1, et_no_edges, parallel));
}
PASS();
}

/* Java Retrofit: the named `value =` form and a full URL are read; a
* concatenation is not, so none of its string fragments becomes a path. */
TEST(http_calls_retrofit_java_interface_issue2723) {
static const EtFile f[] = {
{"src/main/java/com/acme/jclient/OrderApi.java",
"package com.acme.jclient;\n\n"
"import retrofit2.Call;\n"
"import retrofit2.http.Body;\n"
"import retrofit2.http.DELETE;\n"
"import retrofit2.http.GET;\n"
"import retrofit2.http.POST;\n"
"import retrofit2.http.Path;\n\n"
"public interface OrderApi {\n"
" String BASE = \"v1\";\n\n"
" @GET(\"v1/orders/{id}\")\n"
" Call<String> getOrder(@Path(\"id\") String id);\n\n"
" @POST(value = \"v1/orders\")\n"
" Call<String> createOrder(@Body String body);\n\n"
" @GET(\"https://status.acme.com/v1/health\")\n"
" Call<String> health();\n\n"
" @DELETE(BASE + \"/orders/{id}\")\n"
" Call<Void> deleteOrder(@Path(\"id\") String id);\n"
"}\n"}};
static const EtHandles calls[] = {{"OrderApi.getOrder", "/v1/orders/{id}"},
{"OrderApi.createOrder", "/v1/orders"},
{"OrderApi.health", "https://status.acme.com/v1/health"},
{NULL, NULL}};
for (int parallel = 0; parallel <= 1; parallel++) {
ASSERT_TRUE(et_edges_exact_routes(f, 1, "HTTP_CALLS", calls, parallel, NULL));
ASSERT_TRUE(et_handles_exact(f, 1, et_no_edges, parallel));
}
PASS();
}

/* Control: a JAX-RS interface keeps HANDLES. Its @GET is a marker and the path
* is a sibling @Path, so the Retrofit reading (a verb with an argument) does
* not depend on the declaring type being an interface. */
TEST(handles_jaxrs_interface_not_retrofit_issue2723) {
static const EtFile f[] = {
{"src/main/java/com/acme/api/OrderResource.java",
"package com.acme.api;\n\n"
"import jakarta.ws.rs.GET;\n"
"import jakarta.ws.rs.POST;\n"
"import jakarta.ws.rs.Path;\n\n"
"@Path(\"/v1/orders\")\n"
"public interface OrderResource {\n"
" @GET\n"
" @Path(\"/{id}\")\n"
" String get(String id);\n\n"
" @POST\n"
" String create(String body);\n"
"}\n"}};
static const EtHandles handles[] = {{"OrderResource.get", "/v1/orders/{id}"},
{"OrderResource.create", "/v1/orders"},
{NULL, NULL}};
for (int parallel = 0; parallel <= 1; parallel++) {
ASSERT_TRUE(et_handles_exact(f, 1, handles, parallel));
ASSERT_TRUE(et_edges_exact_routes(f, 1, "HTTP_CALLS", et_no_edges, parallel, NULL));
}
PASS();
}

/* The exact DATA_FLOWS set, caller -> handler by QN tail. route_qns (NULL-terminated) must
* exist, so a test that expects NO flow on a path cannot pass because the caller's or the
* handler's Route was never created; handles is the exact HANDLES count. */
Expand Down Expand Up @@ -2871,6 +2979,9 @@ SUITE(edge_types_probe) {
RUN_TEST(handles_jaxrs_java_relative_path);
RUN_TEST(handles_spring_java_relative_string_not_route);
RUN_TEST(handles_jaxrs_scala);
RUN_TEST(http_calls_retrofit_kotlin_interface_issue2723);
RUN_TEST(http_calls_retrofit_java_interface_issue2723);
RUN_TEST(handles_jaxrs_interface_not_retrofit_issue2723);
RUN_TEST(handles_aspnet_csharp);
RUN_TEST(handles_aspnet_attribute_routes_issue1692);
RUN_TEST(handles_aspnet_attribute_routes_issue1041);
Expand Down
Loading
Loading