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
113 changes: 86 additions & 27 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,8 @@ static const char *annotation_route_method(const char *name) {
}
/* JAX-RS bare-verb annotations (@GET/@POST/...) — path comes from @Path. */
if (strcmp(name, "GET") == 0 || strcmp(name, "POST") == 0 || strcmp(name, "PUT") == 0 ||
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0) {
strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0 ||
strcmp(name, "HEAD") == 0 || strcmp(name, "OPTIONS") == 0) {
return name;
}
return NULL;
Expand Down Expand Up @@ -1605,35 +1606,85 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
return true;
}

/* Scan the annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
* wrapper (and direct children) for a route-mapping annotation. Java/Kotlin
* Spring annotations (@GetMapping, @RequestMapping, ...) live here rather than
* as prev-siblings, so the prev-sibling decorator walk never sees them. */
static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
const CBMLangSpec *spec, const char **out_path,
const char **out_method) {
TSNode modifiers = find_jvm_modifiers(func_node, spec->language);
/* Scan ALL annotation nodes nested in a JVM/C# `modifiers`/`attribute_list`
* wrapper (and direct children), collecting route information from the whole
* set instead of stopping at the first mapping annotation. Java/Kotlin Spring
* annotations (@GetMapping, @RequestMapping, ...) live here rather than as
* prev-siblings, so the prev-sibling decorator walk never sees them.
*
* JAX-RS splits the route across two annotations: the verb comes from a bare
* @GET/@POST/... and the path from a sibling @Path("..."). Returning on the
* first mapping annotation therefore dropped every method-level @Path
* (the @GET matched first, defaulted the path to "/", and @Path was never
* read), and class-level @Path prefixes were never recognized at all. */
static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source,
const CBMLangSpec *spec, const char **out_map_path,
const char **out_method, const char **out_jax_path) {
*out_map_path = NULL;
*out_method = NULL;
*out_jax_path = NULL;

TSNode wrappers[2];
int wn = 0;
TSNode modifiers = find_jvm_modifiers(owner, spec->language);
if (!ts_node_is_null(modifiers)) {
uint32_t mc = ts_node_child_count(modifiers);
for (uint32_t mi = 0; mi < mc; mi++) {
TSNode mchild = ts_node_child(modifiers, mi);
if (cbm_kind_in_set(mchild, spec->decorator_node_types) &&
try_route_from_annotation(a, mchild, source, out_path, out_method)) {
return true;
}
}
wrappers[wn++] = modifiers;
}
/* Direct-child annotations (some grammars attach the annotation as a child
* of the method node rather than under `modifiers`). */
uint32_t cc = ts_node_child_count(func_node);
for (uint32_t ci = 0; ci < cc; ci++) {
TSNode child = ts_node_child(func_node, ci);
if (cbm_kind_in_set(child, spec->decorator_node_types) &&
try_route_from_annotation(a, child, source, out_path, out_method)) {
return true;
wrappers[wn++] = owner;

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);
if (!name) {
continue;
}
if (!*out_jax_path && strcmp(name, "Path") == 0) {
TSNode args = annotation_args_node(child);
if (!ts_node_is_null(args)) {
*out_jax_path = extract_route_path_from_args(a, args, source);
}
continue;
}
if (!*out_method) {
const char *method = annotation_route_method(name);
if (method) {
*out_method = method;
TSNode args = annotation_args_node(child);
if (!ts_node_is_null(args)) {
*out_map_path = extract_route_path_from_args(a, args, source);
}
}
}
}
}
return false;
}

static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source,
const CBMLangSpec *spec, const char **out_path,
const char **out_method) {
const char *map_path = NULL;
const char *method = NULL;
const char *jax_path = NULL;
scan_route_annotations(a, func_node, source, spec, &map_path, &method, &jax_path);
/* Method-level routes still require a verb/mapping annotation; a lone
* @Path (JAX-RS sub-resource locator) is not an endpoint by itself. */
if (!method) {
return false;
}
*out_method = method;
*out_path = map_path ? map_path : (jax_path ? jax_path : "/");
return true;
}

static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const char *source,
Expand Down Expand Up @@ -1702,10 +1753,18 @@ static const char *join_route_paths(CBMArena *a, const char *prefix, const char

static const char *spring_class_route_prefix(CBMArena *a, TSNode class_node, const char *source,
const CBMLangSpec *spec) {
const char *prefix = NULL;
const char *map_path = NULL;
const char *method = NULL;
if (extract_route_from_annotations(a, class_node, source, spec, &prefix, &method)) {
return prefix;
const char *jax_path = NULL;
scan_route_annotations(a, class_node, source, spec, &map_path, &method, &jax_path);
if (map_path) {
return map_path; /* @RequestMapping("/api") and friends */
}
if (jax_path) {
return jax_path; /* JAX-RS class-level @Path("/api") carries no verb */
}
if (method) {
return "/"; /* mapping annotation without a path argument */
}
return NULL;
}
Expand Down
33 changes: 33 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -2728,6 +2728,38 @@ TEST(extract_java_method_annotations_issue382) {
PASS();
}

/* Issue #1005: JAX-RS splits a route across two annotations (@GET carries the
* verb, a sibling @Path carries the path). Returning on the first mapping
* annotation dropped every method-level @Path, and the class-level @Path
* prefix was never recognized at all. */
TEST(extract_java_jaxrs_path_composition_issue1005) {
CBMFileResult *r = extract("import jakarta.ws.rs.GET;\n"
"import jakarta.ws.rs.Path;\n"
"@Path(\"/api/v1/widgets\")\n"
"public class WidgetResource {\n"
" @GET\n"
" public String list() { return \"\"; }\n"
" @GET\n"
" @Path(\"/count\")\n"
" public String count() { return \"\"; }\n"
"}\n",
CBM_LANG_JAVA, "t", "WidgetResource.java");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *list = find_def_by_name(r, "list");
ASSERT_NOT_NULL(list);
ASSERT_NOT_NULL(list->route_path);
ASSERT_STR_EQ(list->route_path, "/api/v1/widgets");
ASSERT_STR_EQ(list->route_method, "GET");
const CBMDefinition *count = find_def_by_name(r, "count");
ASSERT_NOT_NULL(count);
ASSERT_NOT_NULL(count->route_path);
ASSERT_STR_EQ(count->route_path, "/api/v1/widgets/count");
ASSERT_STR_EQ(count->route_method, "GET");
cbm_free_result(r);
PASS();
}

/* Find an in-body call by its raw callee text; returns the call or NULL. */
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
for (int i = 0; i < r->calls.count; i++) {
Expand Down Expand Up @@ -3720,6 +3752,7 @@ SUITE(extraction) {
RUN_TEST(js_index_module_qn_not_collide_with_folder);
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
RUN_TEST(extract_java_no_double_class_qn);
RUN_TEST(extract_go_no_filename_in_module_qn);
RUN_TEST(extract_large_ts_has_functions_issue213);
Expand Down
Loading