@descryy/adapter-go 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 +26 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +247 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +114 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +342 -0
- package/dist/client.js.map +1 -0
- package/dist/extract.d.ts +54 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +871 -0
- package/dist/extract.js.map +1 -0
- package/dist/grpc.d.ts +62 -0
- package/dist/grpc.d.ts.map +1 -0
- package/dist/grpc.js +112 -0
- package/dist/grpc.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/modules.d.ts +37 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +83 -0
- package/dist/modules.js.map +1 -0
- package/dist/parse.d.ts +215 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +890 -0
- package/dist/parse.js.map +1 -0
- package/dist/routes.d.ts +92 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +661 -0
- package/dist/routes.js.map +1 -0
- package/dist/tree-sitter-go.wasm +0 -0
- package/package.json +35 -0
package/dist/routes.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `API_ROUTE` from Go source — DEC-117.
|
|
3
|
+
*
|
|
4
|
+
* API_ROUTE ──SERVES_API──▶ API_ENDPOINT ◀──USES_API── caller
|
|
5
|
+
*
|
|
6
|
+
* ## The receiver is the evidence, never the method name
|
|
7
|
+
*
|
|
8
|
+
* A verb-shaped call is not a route. `r.Get(...)` is written by cache clients,
|
|
9
|
+
* config readers, sync maps and test helpers, and admitting on the name is how
|
|
10
|
+
* the Python client extractor would have minted **2,280 wrong edges** (DEC-132).
|
|
11
|
+
* So the receiver must trace to a routing package, by one of three routes and no
|
|
12
|
+
* others:
|
|
13
|
+
*
|
|
14
|
+
* | shape | evidence |
|
|
15
|
+
* | --- | --- |
|
|
16
|
+
* | `http.HandleFunc("GET /orders/{id}", h)` | the base is a **package alias** the file's import table resolves to a routing import path |
|
|
17
|
+
* | `mux := http.NewServeMux()` … `mux.HandleFunc(…)` | the **construction** is the evidence — assigned from a constructor in a routing package, in the same function |
|
|
18
|
+
* | `v1 := r.Group("/v1")` … `v1.GET(…)` | the **grouping call** is the evidence — `r` was itself already evidence, and `Group` is one of that framework's own group-binding methods |
|
|
19
|
+
* | `r.Methods("GET").Path("/orders").HandlerFunc(h)` | the **fluent chain bottoms out at an identifier** that is itself evidence — gorilla/mux's own shape, unwound one call at a time |
|
|
20
|
+
*
|
|
21
|
+
* A local *called* `router` is not evidence, and neither is a parameter typed as
|
|
22
|
+
* one: binding on the type would admit any function that merely accepts a
|
|
23
|
+
* router, and this adapter has no checker to confirm the type resolves to the
|
|
24
|
+
* package it appears to name. The one deliberate exception is the framework's
|
|
25
|
+
* own group-*callback* parameter (`r.Route("/v1", func(r chi.Router) { … })`):
|
|
26
|
+
* that parameter's type is corroborated by the call site that introduced it —
|
|
27
|
+
* the reader already confirmed `r`'s own outer binding before ever looking at
|
|
28
|
+
* the callback — so it is not the same hazard as trusting an arbitrary
|
|
29
|
+
* function's parameter in isolation.
|
|
30
|
+
*
|
|
31
|
+
* ## A pattern with no verb is refused, not defaulted
|
|
32
|
+
*
|
|
33
|
+
* `mux.HandleFunc("/orders/{id}", h)` matches **every** method. Emitting it as a
|
|
34
|
+
* `GET` invents a fact, and that is DEC-114 exactly — a verb the reader never
|
|
35
|
+
* established became `GET` and manufactured a join reported as a confirmed
|
|
36
|
+
* contract match that did not exist. Emitting one route per verb would be worse:
|
|
37
|
+
* five routes nothing declared. gorilla/mux's own version of the same rule: a
|
|
38
|
+
* chain with no `.Methods(...)` anywhere in it is refused the same way, and a
|
|
39
|
+
* chain whose `.Methods(...)` names several verbs mints one route per verb —
|
|
40
|
+
* that is not a guess, it is the registration's own stated cardinality.
|
|
41
|
+
*
|
|
42
|
+
* ## gorilla/mux: a chain, not a two-argument call
|
|
43
|
+
*
|
|
44
|
+
* `r.Methods(http.MethodPost).Path("/orders").HandlerFunc(h)` nests one
|
|
45
|
+
* `call_expression` inside another, one per fluent step, in any order the
|
|
46
|
+
* caller wrote them. `unwindChain` walks from the outermost call down through
|
|
47
|
+
* each `selector_expression` until it reaches the identifier the whole chain
|
|
48
|
+
* is rooted on — the same identifier this file already requires evidence for
|
|
49
|
+
* everywhere else. Only the outermost call of a chain is processed (guarded by
|
|
50
|
+
* `isChainHead`); every inner link is visited too, as the walk continues into
|
|
51
|
+
* ordinary children, but is recognised as mid-chain and skipped rather than
|
|
52
|
+
* misread as a call of its own. `.PathPrefix(p).Subrouter()` — gorilla's own
|
|
53
|
+
* grouping idiom — is the two-step case: the whole chain becomes a new router
|
|
54
|
+
* local when assigned, the same way `.Group(p)` does for gin and echo, just
|
|
55
|
+
* one call longer.
|
|
56
|
+
*
|
|
57
|
+
* `RegisterXServer(grpcServer, impl)` — gRPC's service registration — is a
|
|
58
|
+
* categorically different shape: no HTTP verb, no path template written at
|
|
59
|
+
* the call site, evidence split across two files instead of local to one.
|
|
60
|
+
* Read separately, in `grpc.ts`, not by this file.
|
|
61
|
+
*/
|
|
62
|
+
import { baseTypeName } from "./parse.js";
|
|
63
|
+
const FRAMEWORKS = new Map([
|
|
64
|
+
["net/http", { constructors: new Set(["NewServeMux"]), groupAssignMethods: new Set(), groupCallbackMethods: new Set() }],
|
|
65
|
+
[
|
|
66
|
+
"github.com/gin-gonic/gin",
|
|
67
|
+
{ constructors: new Set(["Default", "New"]), groupAssignMethods: new Set(["Group"]), groupCallbackMethods: new Set() },
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"github.com/go-chi/chi",
|
|
71
|
+
{
|
|
72
|
+
constructors: new Set(["NewRouter"]),
|
|
73
|
+
groupAssignMethods: new Set(),
|
|
74
|
+
groupCallbackMethods: new Set(["Route", "Group"]),
|
|
75
|
+
callbackParamType: "Router",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"github.com/go-chi/chi/v5",
|
|
80
|
+
{
|
|
81
|
+
constructors: new Set(["NewRouter"]),
|
|
82
|
+
groupAssignMethods: new Set(),
|
|
83
|
+
groupCallbackMethods: new Set(["Route", "Group"]),
|
|
84
|
+
callbackParamType: "Router",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"github.com/labstack/echo",
|
|
89
|
+
{ constructors: new Set(["New"]), groupAssignMethods: new Set(["Group"]), groupCallbackMethods: new Set() },
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"github.com/labstack/echo/v4",
|
|
93
|
+
{ constructors: new Set(["New"]), groupAssignMethods: new Set(["Group"]), groupCallbackMethods: new Set() },
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"github.com/gorilla/mux",
|
|
97
|
+
// `.PathPrefix(p).Subrouter()` is a two-step chain, not a single group-assign
|
|
98
|
+
// call — handled specially in `bindingFromChain`, not through either set here.
|
|
99
|
+
{ constructors: new Set(["NewRouter"]), groupAssignMethods: new Set(), groupCallbackMethods: new Set() },
|
|
100
|
+
],
|
|
101
|
+
]);
|
|
102
|
+
const GORILLA_MUX = "github.com/gorilla/mux";
|
|
103
|
+
const ROUTING_PACKAGES = new Set(FRAMEWORKS.keys());
|
|
104
|
+
/** `net/http`'s own named verb constants, as gorilla/mux's `.Methods(...)` most often receives them. */
|
|
105
|
+
const HTTP_METHOD_CONSTANTS = new Map([
|
|
106
|
+
["MethodGet", "GET"],
|
|
107
|
+
["MethodHead", "HEAD"],
|
|
108
|
+
["MethodPost", "POST"],
|
|
109
|
+
["MethodPut", "PUT"],
|
|
110
|
+
["MethodPatch", "PATCH"],
|
|
111
|
+
["MethodDelete", "DELETE"],
|
|
112
|
+
["MethodConnect", "CONNECT"],
|
|
113
|
+
["MethodOptions", "OPTIONS"],
|
|
114
|
+
["MethodTrace", "TRACE"],
|
|
115
|
+
]);
|
|
116
|
+
/**
|
|
117
|
+
* Registration methods that take the verb from the pattern string rather than
|
|
118
|
+
* from their own name. `net/http`'s two.
|
|
119
|
+
*/
|
|
120
|
+
const PATTERN_METHODS = new Set(["HandleFunc", "Handle"]);
|
|
121
|
+
/** Registration methods whose *name* is the verb. */
|
|
122
|
+
const VERB_METHODS = new Map(["Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].flatMap((verb) => [
|
|
123
|
+
[verb, verb.toUpperCase()],
|
|
124
|
+
[verb.toUpperCase(), verb.toUpperCase()],
|
|
125
|
+
]));
|
|
126
|
+
/** The text of a string literal, or `undefined` for anything assembled elsewhere. */
|
|
127
|
+
export function literalOf(node) {
|
|
128
|
+
if (node === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (node.type !== "interpreted_string_literal" && node.type !== "raw_string_literal")
|
|
131
|
+
return undefined;
|
|
132
|
+
const text = node.text;
|
|
133
|
+
// A raw literal cannot contain an escape, and an interpreted one that does is
|
|
134
|
+
// not a path this reader should be splitting.
|
|
135
|
+
if (text.includes("\\"))
|
|
136
|
+
return undefined;
|
|
137
|
+
return text.replace(/^["`]|["`]$/g, "");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Walks a `call_expression` down through nested `X.M1(…).M2(…)…Mn(…)` links
|
|
141
|
+
* until it reaches the identifier the whole chain is rooted on.
|
|
142
|
+
*
|
|
143
|
+
* `undefined` for anything that is not a plain method-call chain on a name —
|
|
144
|
+
* an index expression, a parenthesised expression, a call whose callee is not
|
|
145
|
+
* a selector at all. Those are not evidence this file can read either way.
|
|
146
|
+
*/
|
|
147
|
+
function unwindChain(call) {
|
|
148
|
+
const steps = [];
|
|
149
|
+
let current = call;
|
|
150
|
+
for (;;) {
|
|
151
|
+
const callee = current.childForFieldName("function");
|
|
152
|
+
if (callee === null || callee.type !== "selector_expression")
|
|
153
|
+
return undefined;
|
|
154
|
+
const base = callee.childForFieldName("operand");
|
|
155
|
+
const member = callee.childForFieldName("field");
|
|
156
|
+
if (base === null || member === null)
|
|
157
|
+
return undefined;
|
|
158
|
+
steps.unshift({ member: member.text, call: current });
|
|
159
|
+
if (base.type === "identifier")
|
|
160
|
+
return { root: base, steps };
|
|
161
|
+
if (base.type === "call_expression") {
|
|
162
|
+
current = base;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Is `call` the outermost link of its own chain?
|
|
170
|
+
*
|
|
171
|
+
* A chain is walked once, from its outermost call — every inner link is still
|
|
172
|
+
* visited by the generic tree walk (it is a real `call_expression`, wholly
|
|
173
|
+
* unaware it sits inside a bigger one), and without this guard each inner link
|
|
174
|
+
* would be re-processed as if it were its own, shorter chain.
|
|
175
|
+
*/
|
|
176
|
+
function isChainHead(call) {
|
|
177
|
+
const parent = call.parent;
|
|
178
|
+
if (parent === null || parent.type !== "selector_expression")
|
|
179
|
+
return true;
|
|
180
|
+
const operand = parent.childForFieldName("operand");
|
|
181
|
+
if (operand === null || !operand.equals(call))
|
|
182
|
+
return true;
|
|
183
|
+
const grandparent = parent.parent;
|
|
184
|
+
if (grandparent === null || grandparent.type !== "call_expression")
|
|
185
|
+
return true;
|
|
186
|
+
const fn = grandparent.childForFieldName("function");
|
|
187
|
+
return fn === null || !fn.equals(parent);
|
|
188
|
+
}
|
|
189
|
+
/** A string literal, or a `net/http` named verb constant (`http.MethodPost`), upper-cased. */
|
|
190
|
+
function verbTextOf(node, unit) {
|
|
191
|
+
const literal = literalOf(node);
|
|
192
|
+
if (literal !== undefined)
|
|
193
|
+
return literal.toUpperCase();
|
|
194
|
+
if (node === null || node.type !== "selector_expression")
|
|
195
|
+
return undefined;
|
|
196
|
+
const base = node.childForFieldName("operand");
|
|
197
|
+
const field = node.childForFieldName("field");
|
|
198
|
+
if (base === null || base.type !== "identifier" || field === null)
|
|
199
|
+
return undefined;
|
|
200
|
+
if (unit.imports.get(base.text) !== "net/http")
|
|
201
|
+
return undefined;
|
|
202
|
+
return HTTP_METHOD_CONSTANTS.get(field.text);
|
|
203
|
+
}
|
|
204
|
+
/** `{id:[0-9]+}` -> `{id}` — gorilla/mux's own regex-constrained path-variable syntax. */
|
|
205
|
+
function stripGorillaConstraints(path) {
|
|
206
|
+
return path.replace(/\{([^:}]+):[^}]*\}/g, "{$1}");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* What an unwound assignment-RHS chain binds, if anything: a package
|
|
210
|
+
* constructor, a single-call group-assign (gin/echo's `.Group(prefix)`), or
|
|
211
|
+
* gorilla/mux's own two-call `.PathPrefix(prefix).Subrouter()`.
|
|
212
|
+
*
|
|
213
|
+
* `found` is the router-local map built so far in this same top-down pass —
|
|
214
|
+
* Go's declare-before-use rule is what makes a lookup into it, mid-scan,
|
|
215
|
+
* sound rather than order-dependent luck.
|
|
216
|
+
*/
|
|
217
|
+
function bindingFromChain(unwound, unit, found, refusals) {
|
|
218
|
+
const { root, steps } = unwound;
|
|
219
|
+
if (root.type !== "identifier")
|
|
220
|
+
return undefined;
|
|
221
|
+
if (steps.length === 1) {
|
|
222
|
+
const step = steps[0];
|
|
223
|
+
const path = unit.imports.get(root.text);
|
|
224
|
+
const pkgSpec = path === undefined ? undefined : FRAMEWORKS.get(path);
|
|
225
|
+
if (pkgSpec !== undefined && pkgSpec.constructors.has(step.member)) {
|
|
226
|
+
return { importPath: path, prefix: "" };
|
|
227
|
+
}
|
|
228
|
+
const baseLocal = found.get(root.text);
|
|
229
|
+
const spec = baseLocal === undefined ? undefined : FRAMEWORKS.get(baseLocal.importPath);
|
|
230
|
+
if (baseLocal === undefined || spec === undefined || !spec.groupAssignMethods.has(step.member))
|
|
231
|
+
return undefined;
|
|
232
|
+
const arg = step.call.childForFieldName("arguments")?.namedChild(0) ?? null;
|
|
233
|
+
const literal = literalOf(arg);
|
|
234
|
+
if (literal === undefined) {
|
|
235
|
+
refusals.push({
|
|
236
|
+
rawTarget: `${root.text}.${step.member}`,
|
|
237
|
+
reason: "a route group registration whose prefix is not a string literal. Refused rather than guessed: " +
|
|
238
|
+
"the prefix is assembled somewhere else, and half a prefix is an endpoint that does not exist.",
|
|
239
|
+
line: step.call.startPosition.row + 1,
|
|
240
|
+
});
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
return { importPath: baseLocal.importPath, prefix: baseLocal.prefix + literal };
|
|
244
|
+
}
|
|
245
|
+
if (steps.length === 2 && steps[0]?.member === "PathPrefix" && steps[1]?.member === "Subrouter") {
|
|
246
|
+
const baseLocal = found.get(root.text);
|
|
247
|
+
if (baseLocal === undefined || baseLocal.importPath !== GORILLA_MUX)
|
|
248
|
+
return undefined;
|
|
249
|
+
const arg = steps[0].call.childForFieldName("arguments")?.namedChild(0) ?? null;
|
|
250
|
+
const literal = literalOf(arg);
|
|
251
|
+
if (literal === undefined) {
|
|
252
|
+
refusals.push({
|
|
253
|
+
rawTarget: `${root.text}.PathPrefix.Subrouter`,
|
|
254
|
+
reason: "a route group registration whose prefix is not a string literal. Refused rather than guessed: " +
|
|
255
|
+
"the prefix is assembled somewhere else, and half a prefix is an endpoint that does not exist.",
|
|
256
|
+
line: steps[1].call.startPosition.row + 1,
|
|
257
|
+
});
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return { importPath: baseLocal.importPath, prefix: baseLocal.prefix + literal };
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Locals bound to a router, per function body — a constructed root router, or a
|
|
266
|
+
* group bound from one by assignment.
|
|
267
|
+
*
|
|
268
|
+
* Scoped to the enclosing function on purpose: a package-level `var mux = ...`
|
|
269
|
+
* is a different lifetime and a different reader problem, and admitting it here
|
|
270
|
+
* would mean tracking assignment order across files to know whether the name
|
|
271
|
+
* still holds a router at the call site.
|
|
272
|
+
*
|
|
273
|
+
* Single top-down pass, extending `found` as it goes: Go requires a name be
|
|
274
|
+
* declared before it is used, so by the time `v1.GET(…)` is reached, an earlier
|
|
275
|
+
* `v1 := r.Group("/v1")` in the same body has already been recorded.
|
|
276
|
+
*/
|
|
277
|
+
function routerLocals(body, unit, refusals) {
|
|
278
|
+
const found = new Map();
|
|
279
|
+
const visit = (node) => {
|
|
280
|
+
if (node.type === "short_var_declaration" || node.type === "assignment_statement") {
|
|
281
|
+
const left = node.childForFieldName("left");
|
|
282
|
+
const right = node.childForFieldName("right");
|
|
283
|
+
const name = left?.namedChild(0);
|
|
284
|
+
const rhs = right?.namedChild(0);
|
|
285
|
+
if (name !== null && name !== undefined && name.type === "identifier" && rhs !== null && rhs !== undefined && rhs.type === "call_expression") {
|
|
286
|
+
const unwound = unwindChain(rhs);
|
|
287
|
+
if (unwound !== undefined) {
|
|
288
|
+
const bound = bindingFromChain(unwound, unit, found, refusals);
|
|
289
|
+
if (bound !== undefined)
|
|
290
|
+
found.set(name.text, bound);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let i = 0; i < node.namedChildCount; i += 1) {
|
|
295
|
+
const child = node.namedChild(i);
|
|
296
|
+
if (child !== null)
|
|
297
|
+
visit(child);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
visit(body);
|
|
301
|
+
return found;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Split a `net/http` pattern into a verb and a path.
|
|
305
|
+
*
|
|
306
|
+
* Returns `undefined` when the pattern names no method — the refusal that
|
|
307
|
+
* DEC-114 is about. `[METHOD ][HOST]/[PATH]` is the documented shape; only the
|
|
308
|
+
* method half is read here, because a host is a deployment fact rather than a
|
|
309
|
+
* route identity and merging on it would be a guess.
|
|
310
|
+
*/
|
|
311
|
+
function splitPattern(pattern) {
|
|
312
|
+
const space = pattern.indexOf(" ");
|
|
313
|
+
if (space === -1)
|
|
314
|
+
return undefined;
|
|
315
|
+
const method = pattern.slice(0, space).trim();
|
|
316
|
+
const path = pattern.slice(space + 1).trim();
|
|
317
|
+
if (!/^[A-Z]+$/.test(method) || !path.startsWith("/"))
|
|
318
|
+
return undefined;
|
|
319
|
+
return { method, path };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Is every placeholder in this template a plain path parameter?
|
|
323
|
+
*
|
|
324
|
+
* `{id...}` is a multi-segment wildcard and `{$}` anchors the end of a path.
|
|
325
|
+
* **Neither is a path parameter**, and normalising them to `{param}` would merge
|
|
326
|
+
* routes that serve different requests — the identity defect DEC-055 measured,
|
|
327
|
+
* arriving by a different door.
|
|
328
|
+
*/
|
|
329
|
+
function templateIsPlain(path) {
|
|
330
|
+
for (const match of path.matchAll(/\{([^}]*)\}/g)) {
|
|
331
|
+
const inner = match[1] ?? "";
|
|
332
|
+
if (inner === "" || inner === "$" || inner.endsWith("..."))
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A group-callback's own single parameter, split into its name and the
|
|
339
|
+
* package-qualified type it was declared with — or `undefined` when the
|
|
340
|
+
* parameter list is not exactly one plainly-qualified parameter.
|
|
341
|
+
*/
|
|
342
|
+
function soleParameter(list) {
|
|
343
|
+
if (list === null)
|
|
344
|
+
return undefined;
|
|
345
|
+
let found;
|
|
346
|
+
let count = 0;
|
|
347
|
+
for (let i = 0; i < list.namedChildCount; i += 1) {
|
|
348
|
+
const parameter = list.namedChild(i);
|
|
349
|
+
if (parameter === null || parameter.type !== "parameter_declaration")
|
|
350
|
+
continue;
|
|
351
|
+
count += 1;
|
|
352
|
+
const nameNode = parameter.childForFieldName("name");
|
|
353
|
+
const typeText = baseTypeName(parameter.childForFieldName("type"));
|
|
354
|
+
const dot = typeText === undefined ? -1 : typeText.indexOf(".");
|
|
355
|
+
if (nameNode !== null && typeText !== undefined && dot !== -1) {
|
|
356
|
+
found = { name: nameNode.text, qualifier: typeText.slice(0, dot), typeName: typeText.slice(dot + 1) };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return count === 1 ? found : undefined;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Every route registration in one file, with a ledger row for each refusal.
|
|
363
|
+
*
|
|
364
|
+
* Call sites are found by walking function bodies rather than the whole file, so
|
|
365
|
+
* the router-local set is always the one in scope at the call.
|
|
366
|
+
*/
|
|
367
|
+
export function readGoRoutes(root, unit) {
|
|
368
|
+
const declarations = [];
|
|
369
|
+
const refusals = [];
|
|
370
|
+
const readCall = (call, locals) => {
|
|
371
|
+
const callee = call.childForFieldName("function");
|
|
372
|
+
if (callee === null || callee.type !== "selector_expression")
|
|
373
|
+
return;
|
|
374
|
+
const base = callee.childForFieldName("operand");
|
|
375
|
+
const member = callee.childForFieldName("field");
|
|
376
|
+
if (base === null || member === null || base.type !== "identifier")
|
|
377
|
+
return;
|
|
378
|
+
const method = member.text;
|
|
379
|
+
const isPatternMethod = PATTERN_METHODS.has(method);
|
|
380
|
+
const verb = VERB_METHODS.get(method);
|
|
381
|
+
if (!isPatternMethod && verb === undefined)
|
|
382
|
+
return;
|
|
383
|
+
const importPath = unit.imports.get(base.text);
|
|
384
|
+
const viaPackage = importPath !== undefined && ROUTING_PACKAGES.has(importPath);
|
|
385
|
+
const local = locals.get(base.text);
|
|
386
|
+
const line = call.startPosition.row + 1;
|
|
387
|
+
if (!viaPackage && local === undefined) {
|
|
388
|
+
// Only refuse calls that already look like registrations. Every other
|
|
389
|
+
// `x.Get(...)` in the repository is not a route and not a gap, and filing
|
|
390
|
+
// it would inflate the ledger with the 2,280-edge population.
|
|
391
|
+
if (isPatternMethod) {
|
|
392
|
+
refusals.push({
|
|
393
|
+
rawTarget: `${base.text}.${method}`,
|
|
394
|
+
reason: "an HTTP route registration, but the receiver is neither a routing package nor a router " +
|
|
395
|
+
"constructed in this function. Refused rather than guessed: a route admitted on the method " +
|
|
396
|
+
"name alone would claim an endpoint nothing serves.",
|
|
397
|
+
line,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const framework = importPath ?? local.importPath;
|
|
403
|
+
const prefix = local?.prefix ?? "";
|
|
404
|
+
const argument = call.childForFieldName("arguments")?.namedChild(0) ?? null;
|
|
405
|
+
const written = literalOf(argument);
|
|
406
|
+
if (written === undefined) {
|
|
407
|
+
refusals.push({
|
|
408
|
+
rawTarget: `${base.text}.${method}`,
|
|
409
|
+
reason: "an HTTP route registration whose path is not a string literal. Refused rather than guessed: " +
|
|
410
|
+
"the template is assembled somewhere else, and half a template is an endpoint that does not exist.",
|
|
411
|
+
line,
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let verbHere;
|
|
416
|
+
let path;
|
|
417
|
+
if (isPatternMethod) {
|
|
418
|
+
const split = splitPattern(written);
|
|
419
|
+
if (split === undefined) {
|
|
420
|
+
refusals.push({
|
|
421
|
+
rawTarget: written,
|
|
422
|
+
reason: "an HTTP route registration whose pattern names no method, so it matches every verb. " +
|
|
423
|
+
"Refused rather than defaulted to GET: a verb this reader never established would " +
|
|
424
|
+
"manufacture a join and report a contract match that does not exist (DEC-114).",
|
|
425
|
+
line,
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
verbHere = split.method;
|
|
430
|
+
path = split.path;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
verbHere = verb;
|
|
434
|
+
path = written;
|
|
435
|
+
if (!path.startsWith("/")) {
|
|
436
|
+
refusals.push({
|
|
437
|
+
rawTarget: written,
|
|
438
|
+
reason: "an HTTP route registration whose path is not repository-relative.",
|
|
439
|
+
line,
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const template = prefix + path;
|
|
445
|
+
if (!templateIsPlain(template)) {
|
|
446
|
+
refusals.push({
|
|
447
|
+
rawTarget: written,
|
|
448
|
+
reason: "an HTTP route whose template contains a wildcard or an end-of-path anchor rather than a " +
|
|
449
|
+
"path parameter. Refused rather than normalised: collapsing it to {param} would merge routes " +
|
|
450
|
+
"that serve different requests.",
|
|
451
|
+
line,
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
declarations.push({ method: verbHere, template, written, framework, line });
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* `router.Route("/v1", func(r chi.Router) { … })` / `router.Group(func(r chi.Router) { … })`.
|
|
459
|
+
*
|
|
460
|
+
* Returns `true` when the call was a group-callback shape and has already
|
|
461
|
+
* been fully handled (walked with an extended scope, or refused) — the
|
|
462
|
+
* caller must not also visit it as an ordinary call or recurse into it again.
|
|
463
|
+
* Returns `false` for anything else, including a same-named method on a
|
|
464
|
+
* receiver that never qualified as a router in the first place.
|
|
465
|
+
*/
|
|
466
|
+
const tryGroupCallback = (call, locals) => {
|
|
467
|
+
const callee = call.childForFieldName("function");
|
|
468
|
+
if (callee === null || callee.type !== "selector_expression")
|
|
469
|
+
return false;
|
|
470
|
+
const base = callee.childForFieldName("operand");
|
|
471
|
+
const member = callee.childForFieldName("field");
|
|
472
|
+
if (base === null || member === null || base.type !== "identifier")
|
|
473
|
+
return false;
|
|
474
|
+
const local = locals.get(base.text);
|
|
475
|
+
const spec = local === undefined ? undefined : FRAMEWORKS.get(local.importPath);
|
|
476
|
+
if (local === undefined || spec === undefined || !spec.groupCallbackMethods.has(member.text))
|
|
477
|
+
return false;
|
|
478
|
+
const args = call.childForFieldName("arguments");
|
|
479
|
+
const argCount = args?.namedChildCount ?? 0;
|
|
480
|
+
// `Route(pattern, fn)` takes two; `Group(fn)` takes one. Anything else is
|
|
481
|
+
// not this framework's grouping call after all — fall through unhandled.
|
|
482
|
+
if (argCount !== 1 && argCount !== 2)
|
|
483
|
+
return false;
|
|
484
|
+
const funcLit = args?.namedChild(argCount - 1) ?? null;
|
|
485
|
+
if (funcLit === null || funcLit.type !== "func_literal")
|
|
486
|
+
return false;
|
|
487
|
+
const line = call.startPosition.row + 1;
|
|
488
|
+
const rawTarget = `${base.text}.${member.text}`;
|
|
489
|
+
let addedPrefix = "";
|
|
490
|
+
if (argCount === 2) {
|
|
491
|
+
const literal = literalOf(args?.namedChild(0) ?? null);
|
|
492
|
+
if (literal === undefined) {
|
|
493
|
+
refusals.push({
|
|
494
|
+
rawTarget,
|
|
495
|
+
reason: "a grouped route registration whose prefix is not a string literal. Refused rather than guessed: " +
|
|
496
|
+
"the prefix is assembled somewhere else, and half a prefix is an endpoint that does not exist.",
|
|
497
|
+
line,
|
|
498
|
+
});
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
addedPrefix = literal;
|
|
502
|
+
}
|
|
503
|
+
const param = soleParameter(funcLit.childForFieldName("parameters"));
|
|
504
|
+
const paramPath = param === undefined ? undefined : unit.imports.get(param.qualifier);
|
|
505
|
+
const confirmed = param !== undefined && paramPath === local.importPath && param.typeName === spec.callbackParamType;
|
|
506
|
+
if (!confirmed) {
|
|
507
|
+
refusals.push({
|
|
508
|
+
rawTarget,
|
|
509
|
+
reason: "a grouped route registration whose callback parameter this reader could not confirm as the same " +
|
|
510
|
+
"framework's own router type. Refused rather than walked: a parameter merely typed as a router is " +
|
|
511
|
+
"not by itself evidence, and here even the written type could not be matched against the framework's.",
|
|
512
|
+
line,
|
|
513
|
+
});
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
const body = funcLit.childForFieldName("body");
|
|
517
|
+
if (body === null)
|
|
518
|
+
return true;
|
|
519
|
+
const childLocals = new Map(locals);
|
|
520
|
+
childLocals.set(param.name, { importPath: local.importPath, prefix: local.prefix + addedPrefix });
|
|
521
|
+
walkForRoutes(body, childLocals);
|
|
522
|
+
return true;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* gorilla/mux's fluent chain: `.Methods(…)`, `.Path(…)`, `.HandleFunc(…)`,
|
|
526
|
+
* `.Handler(…)`/`.HandlerFunc(…)`, in any order, any subset, rooted at a
|
|
527
|
+
* confirmed gorilla/mux router.
|
|
528
|
+
*
|
|
529
|
+
* Returns `true` once the chain has been fully judged — emitted, or refused —
|
|
530
|
+
* so the caller does not also try it as a two-argument call. Returns `false`
|
|
531
|
+
* for a mid-chain link (guarded by `isChainHead`) and for any chain that
|
|
532
|
+
* never actually attempted a path or a handler at all: `.Name(…)` alone, or
|
|
533
|
+
* a `.PathPrefix(…).Subrouter()` grouping call, are real gorilla/mux calls
|
|
534
|
+
* this file simply is not the reader for — not evidence of an incomplete
|
|
535
|
+
* registration, so not refused either.
|
|
536
|
+
*/
|
|
537
|
+
const tryGorillaChain = (call, locals) => {
|
|
538
|
+
if (!isChainHead(call))
|
|
539
|
+
return false;
|
|
540
|
+
const unwound = unwindChain(call);
|
|
541
|
+
if (unwound === undefined)
|
|
542
|
+
return false;
|
|
543
|
+
const { root, steps } = unwound;
|
|
544
|
+
const local = locals.get(root.text);
|
|
545
|
+
if (local === undefined || local.importPath !== GORILLA_MUX)
|
|
546
|
+
return false;
|
|
547
|
+
let pathAttempted = false;
|
|
548
|
+
let pathLiteral;
|
|
549
|
+
let handlerSeen = false;
|
|
550
|
+
const verbs = [];
|
|
551
|
+
let verbsUnresolved = false;
|
|
552
|
+
for (const step of steps) {
|
|
553
|
+
if (step.member === "Path") {
|
|
554
|
+
pathAttempted = true;
|
|
555
|
+
pathLiteral = literalOf(step.call.childForFieldName("arguments")?.namedChild(0) ?? null);
|
|
556
|
+
}
|
|
557
|
+
else if (step.member === "HandleFunc") {
|
|
558
|
+
pathAttempted = true;
|
|
559
|
+
handlerSeen = true;
|
|
560
|
+
pathLiteral = literalOf(step.call.childForFieldName("arguments")?.namedChild(0) ?? null);
|
|
561
|
+
}
|
|
562
|
+
else if (step.member === "Handler" || step.member === "HandlerFunc") {
|
|
563
|
+
handlerSeen = true;
|
|
564
|
+
}
|
|
565
|
+
else if (step.member === "Methods") {
|
|
566
|
+
const args = step.call.childForFieldName("arguments");
|
|
567
|
+
const count = args?.namedChildCount ?? 0;
|
|
568
|
+
for (let i = 0; i < count; i += 1) {
|
|
569
|
+
const verb = verbTextOf(args?.namedChild(i) ?? null, unit);
|
|
570
|
+
if (verb === undefined)
|
|
571
|
+
verbsUnresolved = true;
|
|
572
|
+
else
|
|
573
|
+
verbs.push(verb);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Any other link (`.Name(…)`, `.Queries(…)`, `.Headers(…)`, `.Schemes(…)`,
|
|
577
|
+
// `.PathPrefix(…)`) narrows matching without bearing on route identity
|
|
578
|
+
// here, or (`.Subrouter()`) is the grouping shape `routerLocals` already
|
|
579
|
+
// owns — intentionally ignored, not evidence of anything unhandled.
|
|
580
|
+
}
|
|
581
|
+
if (!handlerSeen || !pathAttempted)
|
|
582
|
+
return false;
|
|
583
|
+
const line = call.startPosition.row + 1;
|
|
584
|
+
const rawTarget = `${root.text}.${steps.map((step) => step.member).join(".")}(…)`;
|
|
585
|
+
if (pathLiteral === undefined) {
|
|
586
|
+
refusals.push({
|
|
587
|
+
rawTarget,
|
|
588
|
+
reason: "an HTTP route registration whose path is not a string literal. Refused rather than guessed: " +
|
|
589
|
+
"the template is assembled somewhere else, and half a template is an endpoint that does not exist.",
|
|
590
|
+
line,
|
|
591
|
+
});
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (verbsUnresolved) {
|
|
595
|
+
refusals.push({
|
|
596
|
+
rawTarget,
|
|
597
|
+
reason: "an HTTP route registration whose .Methods(...) argument is not a string literal or a " +
|
|
598
|
+
"recognised net/http Method constant. Refused rather than guessed.",
|
|
599
|
+
line,
|
|
600
|
+
});
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (verbs.length === 0) {
|
|
604
|
+
refusals.push({
|
|
605
|
+
rawTarget,
|
|
606
|
+
reason: "an HTTP route registration with no .Methods(...) anywhere in its chain, so it matches every " +
|
|
607
|
+
"verb. Refused rather than defaulted to GET: a verb this reader never established would " +
|
|
608
|
+
"manufacture a join and report a contract match that does not exist (DEC-114).",
|
|
609
|
+
line,
|
|
610
|
+
});
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
const template = local.prefix + stripGorillaConstraints(pathLiteral);
|
|
614
|
+
if (!templateIsPlain(template)) {
|
|
615
|
+
refusals.push({
|
|
616
|
+
rawTarget,
|
|
617
|
+
reason: "an HTTP route whose template contains a wildcard or an end-of-path anchor rather than a " +
|
|
618
|
+
"path parameter. Refused rather than normalised: collapsing it to {param} would merge routes " +
|
|
619
|
+
"that serve different requests.",
|
|
620
|
+
line,
|
|
621
|
+
});
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
for (const method of new Set(verbs)) {
|
|
625
|
+
declarations.push({ method, template, written: pathLiteral, framework: local.importPath, line });
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
};
|
|
629
|
+
const walkForRoutes = (node, locals) => {
|
|
630
|
+
if (node.type === "call_expression") {
|
|
631
|
+
if (tryGroupCallback(node, locals))
|
|
632
|
+
return;
|
|
633
|
+
if (tryGorillaChain(node, locals))
|
|
634
|
+
return;
|
|
635
|
+
readCall(node, locals);
|
|
636
|
+
}
|
|
637
|
+
for (let i = 0; i < node.namedChildCount; i += 1) {
|
|
638
|
+
const child = node.namedChild(i);
|
|
639
|
+
if (child !== null)
|
|
640
|
+
walkForRoutes(child, locals);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const findBodies = (node) => {
|
|
644
|
+
if (node.type === "function_declaration" || node.type === "method_declaration") {
|
|
645
|
+
const body = node.childForFieldName("body");
|
|
646
|
+
if (body !== null) {
|
|
647
|
+
const locals = routerLocals(body, unit, refusals);
|
|
648
|
+
walkForRoutes(body, locals);
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
for (let i = 0; i < node.namedChildCount; i += 1) {
|
|
653
|
+
const child = node.namedChild(i);
|
|
654
|
+
if (child !== null)
|
|
655
|
+
findBodies(child);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
findBodies(root);
|
|
659
|
+
return { declarations, refusals };
|
|
660
|
+
}
|
|
661
|
+
//# sourceMappingURL=routes.js.map
|