module

Noir::TreeSitterKotlinRouteExtractor

Tree-sitter-backed Kotlin route extractor.

Spring-style annotation routing — class-level @RequestMapping-family annotations composing with method-level mapping annotations. Mirrors TreeSitterJavaRouteExtractor but for Kotlin's distinct AST shape (annotations live in modifiers, functions use function_declaration, primary constructors carry DTO fields, etc.).

This file holds the class-tree walking, mapping prefixes and controller/interface/Feign routes; the sibling files in this directory reopen the module with one concern each (STOMP, GraphQL, gateway, WebFlux functional DSL, shared decoding helpers).

Ktor's DSL routing { get("/x") { ... } } is a different authoring style and lives in a separate walker (kotlin_ktor_route_extractor_ts.cr).

Constants

ANNOTATION_VERBS = {"GetMapping" => "GET", "PostMapping" => "POST", "PutMapping" => "PUT", "DeleteMapping" => "DELETE", "PatchMapping" => "PATCH", "RequestMapping" => nil}

Spring mapping annotation names → HTTP verb. nil means look at the annotation's method = argument. Same table as the Java extractor for consistency.

ARMERIA_VERB_ANNOTATIONS = {"Get" => "GET", "Post" => "POST", "Put" => "PUT", "Delete" => "DELETE", "Patch" => "PATCH", "Head" => "HEAD", "Options" => "OPTIONS", "Trace" => "TRACE"}
CONST_DECL_RE = /\bconst\s+val\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*String)?\s*=\s*/

const val X = A + "/b" + Paths.C: string literals and (dotted) constant names joined by +. Only const — annotation arguments must be compile-time constants, so a plain val concatenation can never reach a mapping path. The literal-only regex below would otherwise keep just the first literal of such a line.

KOTLIN_SCALAR_TYPES = TreeSitterJaxRsExtractor::PRIMITIVE_TYPES | Set {"any", "unit"}

Kotlin scalars a resource method binds from a single request value, lower-cased like the Java extractors' PRIMITIVE_TYPES.

KOTLIN_TYPE_DECL_RE = /^\s*(?:(?:public|private|protected|internal|data|enum|sealed|abstract|open|inner|annotation|value|inline|fun|expect|actual|final)\s+)*(?:class|object|interface)\s+([A-Za-z_][A-Za-z0-9_]*)/

class/object/interface after any modifiers (internal object, data object, enum class). A companion object is not a type of its own: its constants are read through the enclosing class.

MAX_ANNOTATION_VALUE_DEPTH = 64

Annotation values never legitimately nest more than a few levels ([arrayOf("/a")]). The shared MAX_AST_DEPTH backstop is too loose for the two value walkers below: their frames are ~10KB in a debug build, so ~900 nested ((...)) / [[...]] already overran the fiber stack and aborted the whole scan.

MAX_CLASS_PREFIXES = 64

ponytail: nested multi-path mappings multiply, so the product is cut at MAX_CLASS_PREFIXES; raise it if a real app ever needs more.

MAX_EXPANDED_CONSTANT_SIZE = 4096

A resolved route constant is a URL path; anything longer is a template chain doubling itself (B = "$A$A", C = "$B$B", ...), which used to grow until the analyzer failed. Such a value is left unexpanded.

MICRONAUT_BODY_WRAPPER_TYPES = TreeSitterMicronautExtractor::REACTIVE_BODY_WRAPPER_TYPES | Set {"flow"}

Flow<Book> is the coroutine spelling of Publisher<Book>.

NEST_PREDICATE_CALL_RE = /\b(?!path\b)[A-Za-z_]\w*\s*\([^()]*\)/

Calls other than path(...) inside a nest predicate — accept(JSON), queryParam("k", "v") — never carry the prefix.

TEMPLATE_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/

Instance methods

annotated?(decl : LibTreeSitter::TSNode, source : String, name : String) : Bool

Whether decl carries @<name> (simple name) in its own modifiers.

Source
expand_constant_interpolations(constants : Hash(String, String), fallback : Hash(String, String) | Nil = nil) : Hash(String, String)

Resolve Kotlin string-template interpolations inside collected constant values, e.g. const val STATIC_URL = "$PUBLIC_URL/static" with PUBLIC_URL = "/public" becomes /public/static. The regex capture in extract_string_constants stores the raw $PUBLIC_URL text, so without this a path built from such a constant keeps the literal $VAR (or, as an inline annotation literal, mis-parses it as a {VAR} path placeholder). Unresolved references (e.g. Spring ${config.property} placeholders) are left untouched. Bounded iterations resolve transitive chains. Names missing from constants are looked up in fallback (another file's constants).

Source
extract_armeria_routes_from(root : LibTreeSitter::TSNode, source : String, dto_index : KotlinDtoIndex = KotlinDtoIndex.new, *, include_callees : Bool = false) : Array(ArmeriaRoute)
Source
extract_controller_interface_implementations_from(root : LibTreeSitter::TSNode, source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil) : Array(ControllerInterfaceImplementation)
Source
extract_function_callee_spans(source : String, file_path : String) : Array(Tuple(Int32, Int32, Array(Tuple(String, String, Int32))))

{start_byte, end_byte, callees} for every function in source, so an adapter that finds a route by annotation offset (Quarkus @Route) can pick the enclosing function's callees.

Source
extract_graphql_routes_from(root : LibTreeSitter::TSNode, source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil) : Array(GraphqlRoute)
Source
extract_interface_routes_from(root : LibTreeSitter::TSNode, source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil) : Hash(String, Array(Route))
Source
extract_jaxrs_application_path_from(root : LibTreeSitter::TSNode, source : String) : String | Nil

@ApplicationPath("/api") on a Kotlin Application subclass.

Source
extract_jaxrs_routes(source : String, dto_index : KotlinDtoIndex = KotlinDtoIndex.new, *, include_callees : Bool = false) : Array(TreeSitterJaxRsExtractor::Route)
Source
extract_jaxrs_routes_from(root : LibTreeSitter::TSNode, source : String, dto_index : KotlinDtoIndex = KotlinDtoIndex.new, *, include_callees : Bool = false) : Array(TreeSitterJaxRsExtractor::Route)
Source
extract_lambda_dsl_routes(source : String, config : TreeSitterJvmLambdaDslExtractor::Config, constants : Hash(String, String) | Nil = nil, *, include_callees : Bool = false) : Array(TreeSitterJvmLambdaDslExtractor::Route)
Source
extract_micronaut_routes(source : String, dto_index : KotlinDtoIndex = KotlinDtoIndex.new, *, include_callees : Bool = false) : Array(TreeSitterMicronautExtractor::Route)
Source
extract_micronaut_routes_from(root : LibTreeSitter::TSNode, source : String, dto_index : KotlinDtoIndex = KotlinDtoIndex.new, *, include_callees : Bool = false) : Array(TreeSitterMicronautExtractor::Route)
Source
extract_routes(source : String, string_constants = Hash(String, String).new) : Array(Route)
Source
extract_routes_from(root : LibTreeSitter::TSNode, source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil) : Array(Route)

_from(root, source) — accept a pre-parsed root so the Kotlin Spring analyzer can amortise the parse across multiple extractions on the same file. Tree lifetime is the caller's responsibility.

Source
extract_stomp_application_prefixes(source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil) : Array(String)
Source
extract_stomp_routes_from(root : LibTreeSitter::TSNode, source : String, string_constants = Hash(String, String).new, local_string_constants : Hash(String, String) | Nil = nil, application_prefixes = [""]) : Array(Route)
Source
extract_string_constants(source : String) : Hash(String, String)
Source
function_name_row(func : LibTreeSitter::TSNode) : Int32 | Nil

0-based row of the function name: the signature line, below the annotation stack that node_start_row(func) would report.

Source

Nested types