@descryy/adapter-kotlin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Retrofit — the client-side declarative HTTP interface, Kotlin/Android's
3
+ * dominant client-call shape.
4
+ *
5
+ * `language-problems.md` §9's second row: neither Retrofit nor OkHttp had a
6
+ * reader at all, disclosed only by omission from `frameworkExtractors`.
7
+ *
8
+ * ## Structurally different from `client.ts`
9
+ *
10
+ * Every other client-call mechanism this project reads — `client.get(path)`
11
+ * in `client.ts`, `.newCall(request).execute()` for raw OkHttp — is a call
12
+ * SITE: an expression that, when it runs, performs the request, with an
13
+ * enclosing function this reader can blame.
14
+ *
15
+ * A Retrofit interface method has no such site:
16
+ *
17
+ * ```kotlin
18
+ * interface PokedexService {
19
+ * @GET("pokemon/{name}")
20
+ * suspend fun fetchPokemonInfo(@Path("name") name: String): ApiResponse<PokemonInfo>
21
+ * }
22
+ * ```
23
+ *
24
+ * `fetchPokemonInfo` has no body — Retrofit fills it in at runtime with a
25
+ * generated proxy this reader will never see, the same "no method body, no
26
+ * AST call site to read at all" shape `adapter-java`'s Feign/`@HttpExchange`
27
+ * row names, and correctly declines to build for lack of a corpus. Kotlin's
28
+ * corpus is not empty (`skydoves/Pokedex`, real, widely-used as an Android
29
+ * reference app: 2 endpoints across 2 methods), which is the only reason this
30
+ * one is built and that one is not.
31
+ *
32
+ * So the edge starts at the ANNOTATED FUNCTION'S OWN id, not at whatever
33
+ * calls it — the declaration itself is the evidence that calling this
34
+ * function reaches this endpoint, and ordinary `CALLS` resolution (already
35
+ * built, unchanged here) is what connects a real caller to it, one hop away.
36
+ *
37
+ * ## Why raw OkHttp (`OkHttpClient().newCall(...)`) is measured, not built
38
+ *
39
+ * Two real corpora were checked. `tivi` (a large, real, actively maintained
40
+ * Android app) imports `okhttp3.OkHttpClient` in 6 files — **all 6 are
41
+ * dependency-injection configuration** (`OkHttpClient.Builder()...build()`
42
+ * handed to Retrofit), **zero** are a `.newCall(request).execute()` call
43
+ * site. `okhttp` itself has real call sites in `samples/guide` — but every
44
+ * one builds its `Request` in a `val request = Request.Builder().url(...)`
45
+ * two hops from the call, needing the same cross-statement value tracing
46
+ * `language-problems.md` §0.1 already lists as the class of gap this project
47
+ * defers everywhere else (Rust's default trait bodies, PHP's raw `curl_*`) —
48
+ * and every one of those `.url(...)` arguments is an absolute external host
49
+ * (`https://httpbin.org/...`), which this project refuses on sight (DEC-014)
50
+ * regardless. Building the trace would move zero edges on either corpus on
51
+ * disk; this is the same call `adapter-csharp` made for Refit (measured at
52
+ * zero, disclosed rather than built) and `adapter-rust` made for warp/rocket
53
+ * — a rule with no witness in evidence is an untaken branch, not a weak one.
54
+ */
55
+ import type { Node } from "@descryy/adapter-treesitter";
56
+ import type { KotlinImport } from "./parse.ts";
57
+ export interface KotlinRetrofitEndpoint {
58
+ readonly method: string;
59
+ /** Repository-relative, leading slash added if Retrofit's own convention omitted one. */
60
+ readonly path: string;
61
+ readonly written: string;
62
+ /** The owning interface/class, outermost first — matches `KotlinFunc.owners`. */
63
+ readonly owners: readonly string[];
64
+ readonly funcName: string;
65
+ readonly line: number;
66
+ }
67
+ export interface KotlinRetrofitRefusal {
68
+ readonly rawTarget: string;
69
+ readonly reason: string;
70
+ readonly line: number;
71
+ }
72
+ export interface KotlinRetrofitEndpoints {
73
+ readonly declarations: readonly KotlinRetrofitEndpoint[];
74
+ readonly refusals: readonly KotlinRetrofitRefusal[];
75
+ }
76
+ /** Every Retrofit endpoint declaration anywhere in this file. */
77
+ export declare function readRetrofitEndpoints(root: Node, imports: readonly KotlinImport[]): KotlinRetrofitEndpoints;
78
+ //# sourceMappingURL=retrofit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrofit.d.ts","sourceRoot":"","sources":["../src/retrofit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAe/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACzD,QAAQ,CAAC,QAAQ,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACrD;AAqED,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,uBAAuB,CA4E3G"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Retrofit — the client-side declarative HTTP interface, Kotlin/Android's
3
+ * dominant client-call shape.
4
+ *
5
+ * `language-problems.md` §9's second row: neither Retrofit nor OkHttp had a
6
+ * reader at all, disclosed only by omission from `frameworkExtractors`.
7
+ *
8
+ * ## Structurally different from `client.ts`
9
+ *
10
+ * Every other client-call mechanism this project reads — `client.get(path)`
11
+ * in `client.ts`, `.newCall(request).execute()` for raw OkHttp — is a call
12
+ * SITE: an expression that, when it runs, performs the request, with an
13
+ * enclosing function this reader can blame.
14
+ *
15
+ * A Retrofit interface method has no such site:
16
+ *
17
+ * ```kotlin
18
+ * interface PokedexService {
19
+ * @GET("pokemon/{name}")
20
+ * suspend fun fetchPokemonInfo(@Path("name") name: String): ApiResponse<PokemonInfo>
21
+ * }
22
+ * ```
23
+ *
24
+ * `fetchPokemonInfo` has no body — Retrofit fills it in at runtime with a
25
+ * generated proxy this reader will never see, the same "no method body, no
26
+ * AST call site to read at all" shape `adapter-java`'s Feign/`@HttpExchange`
27
+ * row names, and correctly declines to build for lack of a corpus. Kotlin's
28
+ * corpus is not empty (`skydoves/Pokedex`, real, widely-used as an Android
29
+ * reference app: 2 endpoints across 2 methods), which is the only reason this
30
+ * one is built and that one is not.
31
+ *
32
+ * So the edge starts at the ANNOTATED FUNCTION'S OWN id, not at whatever
33
+ * calls it — the declaration itself is the evidence that calling this
34
+ * function reaches this endpoint, and ordinary `CALLS` resolution (already
35
+ * built, unchanged here) is what connects a real caller to it, one hop away.
36
+ *
37
+ * ## Why raw OkHttp (`OkHttpClient().newCall(...)`) is measured, not built
38
+ *
39
+ * Two real corpora were checked. `tivi` (a large, real, actively maintained
40
+ * Android app) imports `okhttp3.OkHttpClient` in 6 files — **all 6 are
41
+ * dependency-injection configuration** (`OkHttpClient.Builder()...build()`
42
+ * handed to Retrofit), **zero** are a `.newCall(request).execute()` call
43
+ * site. `okhttp` itself has real call sites in `samples/guide` — but every
44
+ * one builds its `Request` in a `val request = Request.Builder().url(...)`
45
+ * two hops from the call, needing the same cross-statement value tracing
46
+ * `language-problems.md` §0.1 already lists as the class of gap this project
47
+ * defers everywhere else (Rust's default trait bodies, PHP's raw `curl_*`) —
48
+ * and every one of those `.url(...)` arguments is an absolute external host
49
+ * (`https://httpbin.org/...`), which this project refuses on sight (DEC-014)
50
+ * regardless. Building the trace would move zero edges on either corpus on
51
+ * disk; this is the same call `adapter-csharp` made for Refit (measured at
52
+ * zero, disclosed rather than built) and `adapter-rust` made for warp/rocket
53
+ * — a rule with no witness in evidence is an untaken branch, not a weak one.
54
+ */
55
+ const HTTP_PACKAGE = "retrofit2.http";
56
+ /** Retrofit's own verb annotations — each carries the path as its own argument, unlike Spring's split prefix/suffix. */
57
+ const VERBS = new Map([
58
+ ["GET", "GET"],
59
+ ["POST", "POST"],
60
+ ["PUT", "PUT"],
61
+ ["PATCH", "PATCH"],
62
+ ["DELETE", "DELETE"],
63
+ ["HEAD", "HEAD"],
64
+ ["OPTIONS", "OPTIONS"],
65
+ ]);
66
+ const namedChildren = (node) => {
67
+ const out = [];
68
+ for (let i = 0; i < node.namedChildCount; i += 1) {
69
+ const child = node.namedChild(i);
70
+ if (child !== null)
71
+ out.push(child);
72
+ }
73
+ return out;
74
+ };
75
+ const firstOfType = (node, type) => namedChildren(node).find((child) => child.type === type);
76
+ const lineOf = (node) => node.startPosition.row + 1;
77
+ function importsRetrofitHttp(imports) {
78
+ return imports.some((i) => (i.isWildcard ? i.path === HTTP_PACKAGE : i.path.startsWith(`${HTTP_PACKAGE}.`)));
79
+ }
80
+ /** A plain string literal only — the same whitelist rule every other reader in this adapter applies. */
81
+ function stringLiteralOf(node) {
82
+ if (node === undefined || node.type !== "string_literal")
83
+ return undefined;
84
+ let content = "";
85
+ for (const child of namedChildren(node)) {
86
+ if (child.type !== "string_content")
87
+ return undefined;
88
+ content += child.text;
89
+ }
90
+ return content;
91
+ }
92
+ /** The first Retrofit verb annotation on a declaration's own `modifiers`, if any. */
93
+ function verbAnnotationOf(node) {
94
+ const modifiers = firstOfType(node, "modifiers");
95
+ if (modifiers === undefined)
96
+ return undefined;
97
+ for (const child of namedChildren(modifiers)) {
98
+ if (child.type !== "annotation")
99
+ continue;
100
+ const invocation = firstOfType(child, "constructor_invocation");
101
+ const userType = invocation === undefined ? firstOfType(child, "user_type") : firstOfType(invocation, "user_type");
102
+ const name = userType === undefined ? undefined : firstOfType(userType, "type_identifier")?.text;
103
+ if (name === undefined)
104
+ continue;
105
+ const verb = VERBS.get(name);
106
+ if (verb === undefined)
107
+ continue;
108
+ const valueArgs = invocation === undefined ? undefined : firstOfType(invocation, "value_arguments");
109
+ const firstArg = valueArgs === undefined ? undefined : firstOfType(valueArgs, "value_argument");
110
+ // Retrofit's verb annotations take one POSITIONAL argument — never named.
111
+ const value = firstArg === undefined ? undefined : namedChildren(firstArg)[0];
112
+ return { verb, arg: value, hasArgument: firstArg !== undefined, line: lineOf(child) };
113
+ }
114
+ return undefined;
115
+ }
116
+ /** `pokemon/{name}` -> `/pokemon/{name}` — Retrofit's own paths are written relative with no leading slash. */
117
+ function withLeadingSlash(path) {
118
+ return path.startsWith("/") ? path : `/${path}`;
119
+ }
120
+ const ABSOLUTE_URL = /^[a-zA-Z][\w+.-]*:\/\//;
121
+ /** Every Retrofit endpoint declaration anywhere in this file. */
122
+ export function readRetrofitEndpoints(root, imports) {
123
+ const declarations = [];
124
+ const refusals = [];
125
+ if (!importsRetrofitHttp(imports))
126
+ return { declarations, refusals };
127
+ const walk = (node, owners) => {
128
+ for (const child of namedChildren(node)) {
129
+ if (child.type === "class_declaration" || child.type === "object_declaration") {
130
+ const name = firstOfType(child, "type_identifier")?.text;
131
+ const body = namedChildren(child).find((c) => c.type === "class_body" || c.type === "enum_class_body");
132
+ if (name !== undefined && body !== undefined)
133
+ walk(body, [...owners, name]);
134
+ continue;
135
+ }
136
+ if (child.type === "lambda_literal" || child.type === "object_literal") {
137
+ // Mirrors `parse.ts`'s own guards: a declaration inside a lambda or an
138
+ // anonymous object has no stable owner path to attribute an endpoint to.
139
+ continue;
140
+ }
141
+ if (child.type === "function_declaration") {
142
+ const verbAnnotation = verbAnnotationOf(child);
143
+ if (verbAnnotation === undefined)
144
+ continue;
145
+ const funcName = firstOfType(child, "simple_identifier")?.text;
146
+ if (funcName === undefined)
147
+ continue;
148
+ if (!verbAnnotation.hasArgument) {
149
+ // Retrofit requires the path on every verb annotation — unlike
150
+ // Spring's "no argument means the class prefix", a bare `@GET`
151
+ // names no endpoint at all and cannot be widened to one.
152
+ refusals.push({
153
+ rawTarget: `@${verbAnnotation.verb}`,
154
+ reason: "a Retrofit endpoint annotation with no path argument.",
155
+ line: verbAnnotation.line,
156
+ });
157
+ continue;
158
+ }
159
+ const raw = stringLiteralOf(verbAnnotation.arg);
160
+ if (raw === undefined) {
161
+ refusals.push({
162
+ rawTarget: `@${verbAnnotation.verb}`,
163
+ reason: "a Retrofit endpoint annotation whose path is not a plain string literal. Refused rather " +
164
+ "than guessed: half a template is an endpoint that does not exist.",
165
+ line: verbAnnotation.line,
166
+ });
167
+ continue;
168
+ }
169
+ if (ABSOLUTE_URL.test(raw)) {
170
+ refusals.push({
171
+ rawTarget: raw,
172
+ reason: "a Retrofit endpoint annotation naming an absolute URL, overriding the client's base URL. " +
173
+ "An endpoint is host-free (DEC-014), so this names a host outside this repository.",
174
+ line: verbAnnotation.line,
175
+ });
176
+ continue;
177
+ }
178
+ declarations.push({
179
+ method: verbAnnotation.verb,
180
+ path: withLeadingSlash(raw),
181
+ written: raw,
182
+ owners,
183
+ funcName,
184
+ line: verbAnnotation.line,
185
+ });
186
+ continue;
187
+ }
188
+ if (child.namedChildCount > 0)
189
+ walk(child, owners);
190
+ }
191
+ };
192
+ walk(root, []);
193
+ return { declarations, refusals };
194
+ }
195
+ //# sourceMappingURL=retrofit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrofit.js","sourceRoot":"","sources":["../src/retrofit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `API_ROUTE` from Kotlin source — Ktor's routing DSL.
3
+ *
4
+ * API_ROUTE ──SERVES_API──▶ API_ENDPOINT ◀──USES_API── caller
5
+ *
6
+ * ## Provenance is the block, never the bare name
7
+ *
8
+ * Ktor's routing functions — `get`, `post`, `route`, and the rest — are bare
9
+ * calls with no receiver: `routing { get("/x") { ... } }` reads as an
10
+ * implicit-receiver lambda, the same shape Kotlin uses for scope functions,
11
+ * builders and half of its standard library. `get` alone is not evidence of
12
+ * anything; a config DSL, a map builder or a test fixture can all declare a
13
+ * function named `get`. So the file must first **import** `io.ktor.server
14
+ * .routing.routing` (or the wildcard `io.ktor.server.routing.*`), and only
15
+ * the lambda passed to a bare `routing(...)` call in that file is walked at
16
+ * all. Everything found outside one is not a route and not a gap.
17
+ *
18
+ * ## The path is composed from nesting, and only from nesting
19
+ *
20
+ * `route("/orders") { get("/{id}") { ... } }` registers `GET /orders/{id}`;
21
+ * `route("/orders") { get { ... } }` registers `GET /orders` — no trailing
22
+ * segment at all. Both are read by tracking the accumulated prefix through
23
+ * each `route(...)` a verb call sits inside, never by string-matching the
24
+ * two together after the fact.
25
+ *
26
+ * A call whose name is neither a verb nor `route` — `authenticate(...) {
27
+ * ... }`, `intercept(...) { ... }`, a plain `if` — is not descended into.
28
+ * Ktor's middleware DSL is larger than this reader knows, and stopping at
29
+ * the boundary of what is understood is the disclosed gap; guessing that an
30
+ * unknown wrapper does not consume a path segment would be a claim this
31
+ * reader cannot back up for constructs it has never read.
32
+ */
33
+ import type { Node } from "@descryy/adapter-treesitter";
34
+ import type { KotlinImport } from "./parse.ts";
35
+ export interface KotlinRoute {
36
+ readonly method: string;
37
+ /** The template as composed from nesting, before generic normalisation. */
38
+ readonly template: string;
39
+ readonly written: string;
40
+ readonly framework: string;
41
+ readonly line: number;
42
+ }
43
+ export interface KotlinRouteRefusal {
44
+ readonly rawTarget: string;
45
+ readonly reason: string;
46
+ readonly line: number;
47
+ }
48
+ export interface KotlinRoutes {
49
+ readonly declarations: readonly KotlinRoute[];
50
+ readonly refusals: readonly KotlinRouteRefusal[];
51
+ }
52
+ /** Every route registration in one file, with a ledger row for each refusal. */
53
+ export declare function readKtorRoutes(root: Node, imports: readonly KotlinImport[]): KotlinRoutes;
54
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAgB/C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAClD;AAuHD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,CAkFzF"}
package/dist/routes.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * `API_ROUTE` from Kotlin source — Ktor's routing DSL.
3
+ *
4
+ * API_ROUTE ──SERVES_API──▶ API_ENDPOINT ◀──USES_API── caller
5
+ *
6
+ * ## Provenance is the block, never the bare name
7
+ *
8
+ * Ktor's routing functions — `get`, `post`, `route`, and the rest — are bare
9
+ * calls with no receiver: `routing { get("/x") { ... } }` reads as an
10
+ * implicit-receiver lambda, the same shape Kotlin uses for scope functions,
11
+ * builders and half of its standard library. `get` alone is not evidence of
12
+ * anything; a config DSL, a map builder or a test fixture can all declare a
13
+ * function named `get`. So the file must first **import** `io.ktor.server
14
+ * .routing.routing` (or the wildcard `io.ktor.server.routing.*`), and only
15
+ * the lambda passed to a bare `routing(...)` call in that file is walked at
16
+ * all. Everything found outside one is not a route and not a gap.
17
+ *
18
+ * ## The path is composed from nesting, and only from nesting
19
+ *
20
+ * `route("/orders") { get("/{id}") { ... } }` registers `GET /orders/{id}`;
21
+ * `route("/orders") { get { ... } }` registers `GET /orders` — no trailing
22
+ * segment at all. Both are read by tracking the accumulated prefix through
23
+ * each `route(...)` a verb call sits inside, never by string-matching the
24
+ * two together after the fact.
25
+ *
26
+ * A call whose name is neither a verb nor `route` — `authenticate(...) {
27
+ * ... }`, `intercept(...) { ... }`, a plain `if` — is not descended into.
28
+ * Ktor's middleware DSL is larger than this reader knows, and stopping at
29
+ * the boundary of what is understood is the disclosed gap; guessing that an
30
+ * unknown wrapper does not consume a path segment would be a claim this
31
+ * reader cannot back up for constructs it has never read.
32
+ */
33
+ const ROUTING_PACKAGE = "io.ktor.server.routing";
34
+ const ROUTING_FUNCTION = `${ROUTING_PACKAGE}.routing`;
35
+ /** Ktor's verb functions, and the HTTP method each one names. */
36
+ const VERB_METHODS = new Map([
37
+ ["get", "GET"],
38
+ ["post", "POST"],
39
+ ["put", "PUT"],
40
+ ["patch", "PATCH"],
41
+ ["delete", "DELETE"],
42
+ ["head", "HEAD"],
43
+ ["options", "OPTIONS"],
44
+ ]);
45
+ const namedChildren = (node) => {
46
+ const out = [];
47
+ for (let i = 0; i < node.namedChildCount; i += 1) {
48
+ const child = node.namedChild(i);
49
+ if (child !== null)
50
+ out.push(child);
51
+ }
52
+ return out;
53
+ };
54
+ const firstOfType = (node, type) => namedChildren(node).find((child) => child.type === type);
55
+ // `firstDescendantOfType` lived here to serve the interpolation blacklist and
56
+ // was deleted with it — see `stringLiteralOf`. A descendant search for a
57
+ // *forbidden* node is the shape of that defect, not a helper worth keeping.
58
+ /** The text of a plain string literal — `"/orders"` — or `undefined` for anything with interpolation. */
59
+ function stringLiteralOf(argument) {
60
+ if (argument === undefined)
61
+ return undefined;
62
+ const literal = argument.type === "string_literal" ? argument : firstOfType(argument, "string_literal");
63
+ if (literal === undefined)
64
+ return undefined;
65
+ // **Every child must be plain content — a whitelist, never a blacklist.**
66
+ //
67
+ // This previously named the one interpolation node it knew about
68
+ // (`interpolated_expression`, the `${...}` form) and accepted anything else.
69
+ // Kotlin's grammar has a second: `$name` without braces parses as
70
+ // `interpolated_identifier`. That form fell straight through the check, and
71
+ // since the reader then took only the FIRST `string_content`, `get("/reports/$name")`
72
+ // was admitted as the route `GET /reports/` — a silently truncated template,
73
+ // an endpoint nothing serves, which is precisely what this function's own
74
+ // refusal reason says must never happen. Neither the unit tests nor the
75
+ // fixture caught it because both used the braced form.
76
+ //
77
+ // `adapter-rust` and `adapter-swift` were written with the whitelist shape
78
+ // and were never exposed to it. Enumerating what is allowed cannot be
79
+ // outflanked by a grammar node this reader has not heard of; enumerating
80
+ // what is forbidden always can.
81
+ let content = "";
82
+ for (const child of namedChildren(literal)) {
83
+ if (child.type !== "string_content")
84
+ return undefined;
85
+ content += child.text;
86
+ }
87
+ return content;
88
+ }
89
+ /**
90
+ * A call with args AND a trailing lambda parses as two nested
91
+ * `call_expression`s — `get("/{id}") { ... }` is
92
+ * `call_expression(call_expression(get, call_suffix(args)), call_suffix(lambda))`.
93
+ * A call with only a lambda — `post { ... }` — is a single level. Both are
94
+ * unwrapped here into one shape so the walker does not need to know which it got.
95
+ */
96
+ function parseCall(node) {
97
+ const children = namedChildren(node);
98
+ const head = children[0];
99
+ if (head === undefined)
100
+ return undefined;
101
+ const lambdaBodyOf = (suffix) => {
102
+ if (suffix === undefined || suffix.type !== "call_suffix")
103
+ return undefined;
104
+ const annotated = firstOfType(suffix, "annotated_lambda");
105
+ const lambda = annotated === undefined ? undefined : firstOfType(annotated, "lambda_literal");
106
+ return lambda === undefined ? undefined : firstOfType(lambda, "statements");
107
+ };
108
+ if (head.type === "simple_identifier") {
109
+ const suffix = children[1];
110
+ const args = suffix?.type === "call_suffix" ? firstOfType(suffix, "value_arguments") : undefined;
111
+ const firstArg = args === undefined ? undefined : firstOfType(args, "value_argument");
112
+ return { name: head.text, arg: firstArg, lambdaBody: lambdaBodyOf(suffix) };
113
+ }
114
+ if (head.type === "call_expression") {
115
+ const inner = parseCall(head);
116
+ if (inner === undefined)
117
+ return undefined;
118
+ return { name: inner.name, arg: inner.arg, lambdaBody: lambdaBodyOf(children[1]) };
119
+ }
120
+ return undefined;
121
+ }
122
+ /** `route("/orders") { get { ... } }` composes to `/orders`; a bare verb under no `route` composes to just its own arg. */
123
+ function composePath(prefix, segment) {
124
+ if (segment === undefined || segment === "")
125
+ return prefix;
126
+ if (segment.startsWith("/"))
127
+ return prefix + segment;
128
+ return `${prefix}/${segment}`;
129
+ }
130
+ /**
131
+ * Is every placeholder a plain path parameter?
132
+ *
133
+ * `{...}` is Ktor's unnamed tailcard and `{param?}` is an optional segment —
134
+ * neither pins down one physical request the way `{param}` does, and
135
+ * collapsing either to `{param}` would merge routes that serve different
136
+ * requests, DEC-055's finding arriving in Ktor's own syntax. A bare `*`
137
+ * (single-segment wildcard) is refused for the same reason.
138
+ */
139
+ function templateIsPlain(path) {
140
+ if (path.includes("*"))
141
+ return false;
142
+ for (const match of path.matchAll(/\{([^}]*)\}/g)) {
143
+ const inner = match[1] ?? "";
144
+ if (inner === "" || inner.endsWith("...") || inner.endsWith("?"))
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+ function importsKtorRouting(imports) {
150
+ return imports.some((i) => i.path === ROUTING_FUNCTION || (i.path === ROUTING_PACKAGE && i.isWildcard));
151
+ }
152
+ /** Every route registration in one file, with a ledger row for each refusal. */
153
+ export function readKtorRoutes(root, imports) {
154
+ const declarations = [];
155
+ const refusals = [];
156
+ if (!importsKtorRouting(imports))
157
+ return { declarations, refusals };
158
+ const line = (node) => node.startPosition.row + 1;
159
+ const walkScope = (statements, prefix) => {
160
+ for (const stmt of namedChildren(statements)) {
161
+ const call = stmt.type === "call_expression" ? stmt : undefined;
162
+ if (call === undefined)
163
+ continue;
164
+ const parsed = parseCall(call);
165
+ if (parsed === undefined)
166
+ continue;
167
+ if (parsed.name === "route") {
168
+ const written = stringLiteralOf(parsed.arg);
169
+ if (written === undefined) {
170
+ refusals.push({
171
+ rawTarget: "route(...)",
172
+ reason: "a route(...) block whose path is not a plain string literal. Refused rather than guessed: " +
173
+ "half a prefix is every route nested inside it reported at the wrong path.",
174
+ line: line(call),
175
+ });
176
+ continue;
177
+ }
178
+ if (parsed.lambdaBody !== undefined)
179
+ walkScope(parsed.lambdaBody, composePath(prefix, written));
180
+ continue;
181
+ }
182
+ const method = VERB_METHODS.get(parsed.name);
183
+ if (method === undefined)
184
+ continue; // not a verb, not `route` — an unknown wrapper, not descended into
185
+ const arg = parsed.arg === undefined ? "" : stringLiteralOf(parsed.arg);
186
+ if (arg === undefined) {
187
+ refusals.push({
188
+ rawTarget: parsed.name,
189
+ reason: "an HTTP route registration whose path is not a plain string literal (an interpolated string, " +
190
+ "most likely). Refused rather than guessed: half a template is an endpoint that does not exist.",
191
+ line: line(call),
192
+ });
193
+ continue;
194
+ }
195
+ const template = composePath(prefix, arg === "" ? undefined : arg);
196
+ if (template === "") {
197
+ refusals.push({
198
+ rawTarget: parsed.name,
199
+ reason: "an HTTP route with no path anywhere in its nesting.",
200
+ line: line(call),
201
+ });
202
+ continue;
203
+ }
204
+ if (!templateIsPlain(template)) {
205
+ refusals.push({
206
+ rawTarget: template,
207
+ reason: "an HTTP route whose template contains a wildcard, tailcard or optional segment rather than a " +
208
+ "plain path parameter. Refused rather than normalised: collapsing it to {param} would merge " +
209
+ "routes that serve different requests.",
210
+ line: line(call),
211
+ });
212
+ continue;
213
+ }
214
+ declarations.push({ method, template, written: template, framework: "ktor", line: line(call) });
215
+ }
216
+ };
217
+ const findRoutingBlocks = (node) => {
218
+ if (node.type === "call_expression") {
219
+ const parsed = parseCall(node);
220
+ if (parsed?.name === "routing" && parsed.lambdaBody !== undefined) {
221
+ walkScope(parsed.lambdaBody, "");
222
+ return; // a routing{} block's own body is fully handled by walkScope
223
+ }
224
+ }
225
+ for (const child of namedChildren(node))
226
+ findRoutingBlocks(child);
227
+ };
228
+ findRoutingBlocks(root);
229
+ return { declarations, refusals };
230
+ }
231
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `API_ROUTE` from Kotlin source — Spring MVC and Spring WebFlux annotations.
3
+ *
4
+ * `language-problems.md` §9's first row: `frameworkExtractors` names only
5
+ * `ktor`, and Kotlin-on-Spring — Spring Initializr's own default for a Kotlin
6
+ * backend, and the framework `spring-petclinic-kotlin` is written against —
7
+ * produced nothing at all, disclosed only in `adapter.ts`'s prose rather than
8
+ * in the refusal ledger.
9
+ *
10
+ * This is a direct port of `adapter-java/src/spring.ts`'s admission rules —
11
+ * DEC-127's class-prefix/method-suffix join, and the `@Controller` vs.
12
+ * `@RestController` view/data distinction — onto Kotlin's own annotation
13
+ * grammar, which is shaped differently from Java's: an annotation with no
14
+ * arguments is a bare `user_type` (`@RestController`), and one with arguments
15
+ * is a `constructor_invocation` (`@RequestMapping("/orders")`) — never one
16
+ * grammar node for both, the way Java's is.
17
+ *
18
+ * ## Measured, not assumed
19
+ *
20
+ * `spring-petclinic-kotlin` (the official Spring reference app in Kotlin) is
21
+ * the only real corpus on disk: 6 controllers, 26 mapping annotations, **all
22
+ * of them `@Controller`, none `@RestController`, and every handler returns
23
+ * `String`** (a Thymeleaf view name). Correctly refused as view routes, this
24
+ * reader emits **zero** `API_ROUTE`s from it — the same shape Java's own
25
+ * `spring-petclinic` measured (93.8% view-refused) and the reason
26
+ * `controllerKind` exists at all: admitting all 26 would have been 26 wrong
27
+ * edges, not a degraded number.
28
+ *
29
+ * There is no `@RestController` witness in any corpus on disk, so the "data"
30
+ * path is verified structurally (it is the same rule Java's is, and Java's is
31
+ * measured) and by unit test, not by a second real corpus. Disclosed here
32
+ * rather than left implicit.
33
+ */
34
+ import type { Node } from "@descryy/adapter-treesitter";
35
+ import type { KotlinImport } from "./parse.ts";
36
+ import type { KotlinRoute } from "./routes.ts";
37
+ /** Every Spring MVC/WebFlux route declared anywhere in this file. */
38
+ export declare function readSpringRoutes(root: Node, imports: readonly KotlinImport[]): readonly KotlinRoute[];
39
+ //# sourceMappingURL=spring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spring.d.ts","sourceRoot":"","sources":["../src/spring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqJ/C,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,WAAW,EAAE,CA6DrG"}