Skip to content
Merged
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
68 changes: 62 additions & 6 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,16 @@ static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const
for (uint32_t ai = 0; ai < nc && ai < DECORATOR_SCAN_LIMIT; ai++) {
TSNode arg = ts_node_named_child(args, ai);
const char *ak = ts_node_type(arg);
/* Kotlin wraps each annotation argument in a `value_argument` node
* (and supports the named form `value = "/x"`); unwrap to the string. */
if (strcmp(ak, "value_argument") == 0) {
TSNode s = cbm_find_child_by_kind(arg, "string_literal");
if (ts_node_is_null(s)) {
continue;
}
arg = s;
ak = ts_node_type(arg);
}
if (strcmp(ak, "string") != 0 && strcmp(ak, "string_literal") != 0 &&
strcmp(ak, "interpreted_string_literal") != 0) {
continue;
Expand Down Expand Up @@ -1252,14 +1262,63 @@ static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char
return true;
}

/* Try to extract a route from a Java/JVM annotation node (`annotation` or
/* Resolve an annotation's name node across grammars. Java exposes a `name`
* field; tree-sitter-kotlin does not — its annotation name lives in a nested
* type_identifier:
* @Foo -> (annotation (user_type (type_identifier)))
* @Foo("/x") -> (annotation (constructor_invocation (user_type (type_identifier))
* (value_arguments ...)))
* Returns a null node when no name can be resolved. */
static TSNode annotation_name_node(TSNode annotation) {
TSNode name = ts_node_child_by_field_name(annotation, TS_FIELD("name"));
if (!ts_node_is_null(name)) {
return name;
}
TSNode ut = cbm_find_child_by_kind(annotation, "user_type");
if (ts_node_is_null(ut)) {
TSNode ci = cbm_find_child_by_kind(annotation, "constructor_invocation");
if (!ts_node_is_null(ci)) {
ut = cbm_find_child_by_kind(ci, "user_type");
}
}
if (!ts_node_is_null(ut)) {
TSNode ti = cbm_find_child_by_kind(ut, "type_identifier");
if (ts_node_is_null(ti)) {
ti = cbm_find_child_by_kind(ut, "simple_identifier");
}
return ti;
}
TSNode null_node = {0};
return null_node;
}

/* Resolve an annotation's argument list across grammars. Kotlin keeps the args
* under a `constructor_invocation` child as a `value_arguments` node rather than
* the `arguments` field / `argument_list` child that Java exposes. */
static TSNode annotation_args_node(TSNode annotation) {
TSNode args = ts_node_child_by_field_name(annotation, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
return args;
}
args = find_decorator_args(annotation);
if (!ts_node_is_null(args)) {
return args;
}
TSNode ci = cbm_find_child_by_kind(annotation, "constructor_invocation");
if (!ts_node_is_null(ci)) {
return cbm_find_child_by_kind(ci, "value_arguments");
}
return args;
}

/* Try to extract a route from a Java/JVM/Kotlin annotation node (`annotation` or
* `marker_annotation`). Spring mapping annotations carry the HTTP method in the
* annotation name and the path in the (optional) argument list:
* @GetMapping("/orders") @RequestMapping(value="/api") @PostMapping
* Returns true when the annotation is a route-mapping annotation. */
static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char *source,
const char **out_path, const char **out_method) {
TSNode name_node = ts_node_child_by_field_name(annotation, TS_FIELD("name"));
TSNode name_node = annotation_name_node(annotation);
if (ts_node_is_null(name_node)) {
return false;
}
Expand All @@ -1268,10 +1327,7 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
if (!method) {
return false;
}
TSNode args = ts_node_child_by_field_name(annotation, TS_FIELD("arguments"));
if (ts_node_is_null(args)) {
args = find_decorator_args(annotation);
}
TSNode args = annotation_args_node(annotation);
const char *path = NULL;
if (!ts_node_is_null(args)) {
path = extract_route_path_from_args(a, args, source);
Expand Down
25 changes: 25 additions & 0 deletions tests/test_edge_types_probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,30 @@ TEST(handles_spring_java) {
PASS();
}

/* Spring (Kotlin) — @RequestMapping/@GetMapping on a Kotlin @RestController.
* REAL BUG: tree-sitter-kotlin annotation nodes have no `name` field (the name
* lives in a nested user_type/type_identifier) and carry args under a
* constructor_invocation `value_arguments` node, so the Java-shaped
* ts_node_child_by_field_name(annotation, "name") / "arguments" lookups in
* try_route_from_annotation missed every Kotlin Spring route → route_path never
* set → no Route/HANDLES. Fixed by annotation_name_node/annotation_args_node. */
TEST(handles_spring_kotlin) {
static const EtFile f[] = {
{"OrderController.kt",
"package com.example\n\n"
"import org.springframework.web.bind.annotation.RequestMapping\n"
"import org.springframework.web.bind.annotation.GetMapping\n\n"
"@RequestMapping(\"/api\")\nclass OrderController {\n"
" @GetMapping(\"/orders\")\n"
" fun listOrders(): String {\n"
" return \"orders\"\n }\n\n"
" @GetMapping(\"/orders/{id}\")\n"
" fun getOrder(id: Int): String {\n"
" return \"order:\" + id\n }\n}\n"}};
ASSERT_TRUE(et_edge_present(f, 1, "HANDLES", 1));
PASS();
}

/* ASP.NET Minimal API (C#) — route registration via static MapGet/MapPost calls
* with identifier handlers, under a Microsoft/AspNetCore path so the resolved
* callee QN carries the "MapGet"/"Microsoft.AspNetCore" route-reg substrings.
Expand Down Expand Up @@ -1351,6 +1375,7 @@ SUITE(edge_types_probe) {
RUN_TEST(handles_fastify_js);
RUN_TEST(handles_gin_go);
RUN_TEST(handles_spring_java);
RUN_TEST(handles_spring_kotlin);
RUN_TEST(handles_aspnet_csharp);
RUN_TEST(handles_laravel_php);
RUN_TEST(handles_rails_ruby);
Expand Down
Loading