@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.
- package/dist/adapter.d.ts +27 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +272 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +93 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +344 -0
- package/dist/client.js.map +1 -0
- package/dist/extract.d.ts +56 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +722 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +157 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +571 -0
- package/dist/parse.js.map +1 -0
- package/dist/retrofit.d.ts +78 -0
- package/dist/retrofit.d.ts.map +1 -0
- package/dist/retrofit.js +195 -0
- package/dist/retrofit.js.map +1 -0
- package/dist/routes.d.ts +54 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +231 -0
- package/dist/routes.js.map +1 -0
- package/dist/spring.d.ts +39 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +231 -0
- package/dist/spring.js.map +1 -0
- package/dist/tree-sitter-kotlin.wasm +0 -0
- package/package.json +35 -0
|
@@ -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"}
|
package/dist/retrofit.js
ADDED
|
@@ -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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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -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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"}
|
package/dist/spring.d.ts
ADDED
|
@@ -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"}
|