Skip to content

Spring routes on Kotlin controllers are not extracted (Route nodes empty for @RestController Kotlin classes) #565

Description

@jsw6701

Summary

Spring MVC routing annotations (@RequestMapping, @GetMapping, @PostMapping, …) on Kotlin controllers are silently not turned into routes. The exact same annotations on Java controllers work. As a result a Kotlin Spring backend gets 0 real HTTP routes in the graph, and cross-repo-intelligence produces 0 CROSS_HTTP_CALLS edges into that service even when the calling side has perfectly clean url_path values — because there is nothing to match against on the callee side.

Environment

  • Repo: DeusData/codebase-memory-mcp, main (cloned 2026-06).
  • Reproduced against a real Kotlin + Spring Boot codebase and confirmed by reading internal/cbm/.
  • Mode: full index, then cross-repo-intelligence.

Reproduction

Minimal Kotlin controller (textbook Spring, annotations on the class + methods):

@RestController
@RequestMapping("/api/v1/connectors")
class ConnectorController(private val service: ConnectorService) {

    @PostMapping
    fun createConnector(@RequestBody req: ConnectorCreateRequestDto): ResponseEntity<Results> = ...

    @GetMapping("/{id}")
    fun getConnectorById(@PathVariable id: Long): ResponseEntity<Results> = ...
}

Index the project (full), then query the graph.

Expected

ConnectorController methods produce Route entries, e.g. POST /api/v1/connectors, GET /api/v1/connectors/{id}.

Actual

  • The ConnectorController class node exists, but every method has empty route_method / route_path.
  • The method's decorators property captures custom annotations (e.g. @RequiresIntegratedPermission) but not @PostMapping / @GetMapping.
  • Project-wide: 0 routes with source = "decorator" (only config-file URLs are present, as source = "infra").
  • For contrast, an equivalent Java Spring service in the same workspace yields dozens of decorator-source routes.
  • Downstream: cross-repo-intelligence returns cross_http_calls: 0 for callers that hit this service, even though those callers' HTTP_CALLS edges have correct url_path (e.g. /api/v1/connectors/{id}).

Root cause

internal/cbm/extract_defs.c, try_route_from_annotation() (≈ line 1260):

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"));
    if (ts_node_is_null(name_node)) {
        return false;            // <-- Kotlin always bails out here
    }
    ...
}

The function reads the annotation name via the name field. This is a Java-grammar assumption:

  • tree-sitter-javamarker_annotation / annotation nodes expose a name: field, so the lookup succeeds.
    (lang_specs.c: java_decorator_types[] = {"marker_annotation", "annotation", NULL})
  • tree-sitter-kotlin — the annotation node has no name field. The name is nested:
    • @PostMapping(annotation (user_type (type_identifier)))
    • @RequestMapping("/x")(annotation (constructor_invocation (user_type (type_identifier)) (value_arguments (value_argument (string_literal)))))
      (lang_specs.c: kotlin_decorator_types[] = {"annotation", NULL})

So for every Kotlin annotation ts_node_child_by_field_name(annotation, "name") returns null and the function returns false before annotation_route_method() is ever consulted. No Kotlin Spring route is ever emitted.

Note: find_jvm_modifiers() already routes CBM_LANG_KOTLIN to the "modifiers" wrapper correctly, and extract_route_from_annotations() iterates it — so the scaffolding is in place. The break is solely the name-field assumption inside try_route_from_annotation.

Proposed fix (focused)

In try_route_from_annotation, when the name field is absent, fall back to a nested-identifier lookup so it works for the Kotlin annotation shape:

  1. If name field is null, descend into the annotation node to the first user_type / constructor_invocationtype_identifier (or simple_identifier) and use that as the annotation name.
  2. For arguments: in addition to the arguments field / argument_list child, recognize Kotlin's value_arguments node (and unwrap the value_argument children) in find_decorator_args().
  3. In extract_route_path_from_args(), accept Kotlin's string node kind (string_literal) and descend through the value_argument wrapper.

This keeps the change contained to the three route-extraction helpers; no API/pipeline changes.

Related (separate PRs, not in scope here)

  • Class-level base path is not merged with method-level paths. extract_route_from_decorators operates on the method node only; a class-level @RequestMapping("/api/v1/connectors") is never prepended to method paths. This affects Java too (methods with bare @PostMapping get path /), but is most visible in Kotlin controllers that put the base path on the class. Happy to file separately.

Repro / test

Suggest adding a Kotlin Spring case to tests/test_httplink.c mirroring the existing Java route-extraction test, asserting POST /api/v1/connectors and GET /api/v1/connectors/{id} are emitted.

Happy to submit a PR with the fix + test (DCO-signed, C-only) if this direction looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions