module

Noir::TreeSitterKotlinKtorRouteExtractor

Tree-sitter-backed Ktor DSL route extractor.

Walks the canonical Ktor server idiom:

routing {
  get("/x") { ... }
  route("/api") {
    post("/items") { val item = call.receive < Item > () }
  }
  authenticate("auth-jwt") {
    get("/profile") { ... }
  }
}

Recognises:

  • Verb DSL calls — get/post/put/delete/patch/head/options/query with a string-literal path argument and a trailing lambda body.
  • route("/x") { ... } blocks contributing to the path prefix.
  • authenticate("realm") { ... } blocks acting as transparent wrappers (no prefix change). Tagging is handled elsewhere; we just descend so wrapped routes are still discovered.
  • routing { ... } and application.routing { ... } entry points.
  • Type-safe @Resource routing — get<VideoStream> { }, resource<Login> { post { } }, method(HttpMethod.Get) { handle<T> } — with cross-file parent-path composition.
  • Inside each verb's lambda body:
    • call.receive<T>() → body parameter typed T as json
    • call.parameters["name"] → name parameter as query
    • call.request.headers["name"] → name parameter as header

Not covered yet:

  • install(plugin) { ... } plugin scoping that affects routing.

Constants

DOC_DSL_NAMES = Set {"describe", "documentation", "parameters"}

API-documentation DSL blocks. They hang off a route (get("/x") { }.describe { ... }) or nest inside one, and they describe an endpoint rather than declaring one — no route is ever registered inside them.

They have to be skipped because the parameter sub-DSL names a query parameter with query("name") { ... }, which is exactly the shape of the QUERY verb call (RFC 10008): a name, a trailing lambda, inside the routing scope. Without this, every documented query parameter became a QUERY route named after the parameter — query("chunk_size") in Ktor's own httpbin sample surfaced as QUERY /chunk_size.

Only the lambda is skipped, never the receiver: the block is chained onto the very route it documents, so dropping the whole call would take the real route with it.

HTTP_VERB_NAMES = {"get" => "GET", "post" => "POST", "put" => "PUT", "delete" => "DELETE", "patch" => "PATCH", "head" => "HEAD", "options" => "OPTIONS", "query" => "QUERY", "webSocket" => "GET", "webSocketRaw" => "GET", "sse" => "GET"}
PASSTHROUGH_NAMES = Set {"routing", "authenticate", "rateLimit", "intercept", "host", "port"}

Pass-through DSL calls — descend into their lambda body without changing the path prefix. routing is the entry point; authenticate wraps a sub-tree behind an auth realm; the remaining names cover the common Ktor scoping helpers. install is handled separately (see walk) because only install(Routing) contributes routes — every other plugin config block must NOT be walked as routing.

Instance methods

compose_resource_paths(raws : Array(RawResource)) : Hash(String, String)

Resolve each raw resource to its full URL path. The parent is the first primary-constructor property whose type is itself a resource (root: Root → /api); the child path joins onto it. Returns a map keyed by BOTH the dotted lexical name (Articles.New) and the bare simple name (TagsResource) so get<...> references resolve either way.

Source
extract_resource_classes(source : String) : Array(RawResource)

Collect every @Resource("path")-annotated class/object in a file (including nested ones, with their dotted lexical name). The analyzer gathers these across the whole project before composing full paths, since a resource's parent is often declared in another module (Ktor's KMP commonMain resource definitions).

Source
extract_routes(source : String, string_constants = Hash(String, String).new, resource_paths = Hash(String, String).new, *, include_callees : Bool = false) : Array(Route)
Source
extract_string_constants(source : String) : Hash(String, String)
Source

Nested types