gitnexus 1.6.13-rc.74 → 1.6.13-rc.76
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/core/group/extractors/http-patterns/go.js +493 -23
- package/dist/core/group/extractors/http-patterns/types.d.ts +8 -0
- package/dist/core/group/extractors/http-route-extractor.js +13 -7
- package/dist/core/ingestion/route-extractors/go-gin-echo.js +1 -62
- package/dist/core/ingestion/route-extractors/go-shared.d.ts +23 -0
- package/dist/core/ingestion/route-extractors/go-shared.js +83 -0
- package/package.json +2 -1
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import Go from 'tree-sitter-go';
|
|
2
|
-
import {
|
|
2
|
+
import { goImportPackageName } from '../../../ingestion/languages/go/import-package-name.js';
|
|
3
|
+
import { stringLiteral } from '../../../ingestion/route-extractors/go-shared.js';
|
|
4
|
+
import { compilePatterns, runCompiledPatterns, } from '../tree-sitter-scanner.js';
|
|
3
5
|
/**
|
|
4
6
|
* Go HTTP plugin. Handles:
|
|
5
|
-
* - gin / echo
|
|
7
|
+
* - gin / echo framework routing — `r.GET("/path", handler)`, including
|
|
8
|
+
* prefixes from route groups bound in the same function (`r.Group("/api")`)
|
|
6
9
|
* - net/http stdlib — `http.HandleFunc("/path", handler)`
|
|
7
10
|
* - net/http consumer — `http.Get(...)`, `http.NewRequest("METHOD", ...)`
|
|
8
11
|
* - resty consumer — `client.R().Delete("/path")`
|
|
9
12
|
*/
|
|
10
13
|
// ─── Provider: framework routing ──────────────────────────────────────
|
|
11
|
-
// Matches `\w+\.GET(...)` etc. (gin
|
|
12
|
-
// Captures the HTTP method (field name),
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Matches `\w+\.GET(...)` etc. (gin and echo share this shape).
|
|
15
|
+
// Captures the receiver, the HTTP method (field name), and the path literal
|
|
16
|
+
// (either Go string form; stringLiteral decodes both, as ingestion does). The
|
|
17
|
+
// query does not anchor the path with `.`: tree-sitter counts comments as
|
|
18
|
+
// named children, so `GET(/* c */ "/p", h)` would fail the anchor. scan instead
|
|
19
|
+
// requires the path to be the first argument in code (comments skipped) and
|
|
20
|
+
// picks the handler out of the remaining code arguments. Which argument that is depends on the framework:
|
|
21
|
+
// gin is `GET(path, middleware..., handler)` (last), echo is
|
|
22
|
+
// `GET(path, handler, middleware...)` (first) — see readFrameworkImports and
|
|
23
|
+
// the per-call choice in scan below.
|
|
24
|
+
// The handler must be an identifier, an inline func literal, or a method
|
|
25
|
+
// value / package-qualified function (`h.ListUsers`, `handlers.ListUsers`);
|
|
26
|
+
// anything else there means the call cannot be attributed to a symbol, so it
|
|
27
|
+
// is dropped rather than guessed (variadic-middleware over-match, #2276).
|
|
17
28
|
const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
|
|
18
29
|
name: 'go-framework-route',
|
|
19
30
|
language: Go,
|
|
@@ -23,15 +34,407 @@ const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
|
|
|
23
34
|
query: `
|
|
24
35
|
(call_expression
|
|
25
36
|
function: (selector_expression
|
|
37
|
+
operand: (_) @receiver
|
|
26
38
|
field: (field_identifier) @http_method (#match? @http_method "^(GET|POST|PUT|DELETE|PATCH)$"))
|
|
27
39
|
arguments: (argument_list
|
|
28
|
-
(interpreted_string_literal) @path
|
|
29
|
-
[(identifier) (func_literal)] @handler
|
|
30
|
-
.))
|
|
40
|
+
[(interpreted_string_literal) (raw_string_literal)] @path))
|
|
31
41
|
`,
|
|
32
42
|
},
|
|
33
43
|
],
|
|
34
44
|
});
|
|
45
|
+
/** Named children that are code, not comments (tree-sitter names comments). */
|
|
46
|
+
function codeChildren(node) {
|
|
47
|
+
return node ? node.namedChildren.filter((c) => c.type !== 'comment') : [];
|
|
48
|
+
}
|
|
49
|
+
/** Argument forms a route handler may take. */
|
|
50
|
+
const HANDLER_ARG_TYPES = new Set([
|
|
51
|
+
'identifier',
|
|
52
|
+
'func_literal',
|
|
53
|
+
'selector_expression',
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* The file's framework import aliases: which local qualifiers resolve to
|
|
57
|
+
* echo and to gin. Matched on the import path rather than the local name, so
|
|
58
|
+
* an aliased import still counts; an unaliased import is keyed by its
|
|
59
|
+
* conventional package name (`goImportPackageName`). `_` and `.` imports bind
|
|
60
|
+
* no qualifier this file can route through. An empty set means the file
|
|
61
|
+
* proves nothing about that framework. Echo's verb calls take the handler as
|
|
62
|
+
* the FIRST argument after the path (`GET(path, handler, middleware...)`),
|
|
63
|
+
* gin's as the LAST (`GET(path, middleware..., handler)`) — `scan` picks the
|
|
64
|
+
* rule per call from these sets.
|
|
65
|
+
*/
|
|
66
|
+
function readFrameworkImports(root) {
|
|
67
|
+
// Imports sit only at file scope; skip bodies.
|
|
68
|
+
const specs = root.namedChildren
|
|
69
|
+
.filter((node) => node.type === 'import_declaration')
|
|
70
|
+
.flatMap((decl) => decl.descendantsOfType('import_spec'));
|
|
71
|
+
const echo = new Set();
|
|
72
|
+
const gin = new Set();
|
|
73
|
+
for (const spec of specs) {
|
|
74
|
+
const importPath = stringLiteral(spec.childForFieldName('path'));
|
|
75
|
+
if (importPath === null)
|
|
76
|
+
continue;
|
|
77
|
+
const local = spec.childForFieldName('name')?.text ?? goImportPackageName(importPath);
|
|
78
|
+
if (local === '_' || local === '.')
|
|
79
|
+
continue;
|
|
80
|
+
if (importPath.includes('labstack/echo'))
|
|
81
|
+
echo.add(local);
|
|
82
|
+
else if (importPath.includes('gin-gonic/gin'))
|
|
83
|
+
gin.add(local);
|
|
84
|
+
}
|
|
85
|
+
return { echo, gin };
|
|
86
|
+
}
|
|
87
|
+
// ─── Route groups: `v1 := r.Group("/api/v1")` ─────────────────────────
|
|
88
|
+
// gin (`*gin.RouterGroup`) and echo (`*echo.Group`) routes registered on a
|
|
89
|
+
// group inherit every enclosing `Group(prefix)`. The prefix is recovered by
|
|
90
|
+
// walking the route's receiver back through its bindings, lexically, inside
|
|
91
|
+
// the enclosing function declaration only: a group handed to another
|
|
92
|
+
// function (`registerAdmin(v1)`) arrives as a parameter and contributes no
|
|
93
|
+
// prefix there, and a receiver bound to anything but a literal-prefix
|
|
94
|
+
// `Group(...)` call contributes none either — the route keeps its literal path.
|
|
95
|
+
// Statement-scoped bindings count too: an `if`/`switch` initializer, a `for`
|
|
96
|
+
// clause (including `range`), a type-switch guard, and declarations inside a
|
|
97
|
+
// switch case all scope over their statement the same way Go scopes them, so
|
|
98
|
+
// they shadow an outer group of the same name instead of being skipped. A
|
|
99
|
+
// select case's receive binding (`case g := <-ch:`) stops the walk entirely:
|
|
100
|
+
// what arrives from the channel is statically unknown, so the route keeps its
|
|
101
|
+
// literal path rather than inheriting an outer group.
|
|
102
|
+
const MAX_GROUP_DEPTH = 32;
|
|
103
|
+
function joinRoutePath(prefix, relative) {
|
|
104
|
+
let joined = relative;
|
|
105
|
+
if (prefix && relative) {
|
|
106
|
+
joined = `${prefix.replace(/\/+$/, '')}/${relative.replace(/^\/+/, '')}`;
|
|
107
|
+
}
|
|
108
|
+
else if (prefix) {
|
|
109
|
+
joined = prefix;
|
|
110
|
+
}
|
|
111
|
+
// Collapse duplicate slashes on the FINAL result — every return branch, not
|
|
112
|
+
// just the join — because ingestion's normalizeExtractedRoutePath collapses
|
|
113
|
+
// all "//" while the downstream contract-id normalizer does not: a path
|
|
114
|
+
// that keeps "//" would split into two contract ids across the strategies.
|
|
115
|
+
const collapsed = joined.replace(/\/+/g, '/');
|
|
116
|
+
// Force a leading "/" for the same reason: ingestion's
|
|
117
|
+
// normalizeExtractedRoutePath always adds one, while normalizeHttpPath (the
|
|
118
|
+
// shared contract-id normalizer) does not — a literal "x" or a slashless
|
|
119
|
+
// Group("api") prefix would emit `...::x` here and `...::/x` there. Gin
|
|
120
|
+
// also refuses a registration path that does not start with "/".
|
|
121
|
+
return collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
|
|
122
|
+
}
|
|
123
|
+
/** `parent.Group("/p", mw...)` → its receiver and literal prefix; null otherwise. */
|
|
124
|
+
function asGroupCall(node) {
|
|
125
|
+
if (node.type !== 'call_expression')
|
|
126
|
+
return null;
|
|
127
|
+
const fn = node.childForFieldName('function');
|
|
128
|
+
if (fn?.type !== 'selector_expression' || fn.childForFieldName('field')?.text !== 'Group') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const parent = fn.childForFieldName('operand');
|
|
132
|
+
const first = codeChildren(node.childForFieldName('arguments'))[0];
|
|
133
|
+
// Only a string literal carries a prefix, and it must decode to the text
|
|
134
|
+
// the runtime registers: stringLiteral applies Go unescaping (both `"…"`
|
|
135
|
+
// with escapes and raw `` `…` `` strings). A non-literal argument (a
|
|
136
|
+
// variable, concatenation) or an undecodable string contributes no prefix.
|
|
137
|
+
const prefix = first ? stringLiteral(first) : null;
|
|
138
|
+
if (!parent || prefix === null)
|
|
139
|
+
return null;
|
|
140
|
+
return { parent, prefix };
|
|
141
|
+
}
|
|
142
|
+
/** The expression `name` is assigned by `stmt` (`:=`, `=`, or `var`), if any. */
|
|
143
|
+
function boundValue(stmt, name) {
|
|
144
|
+
const pick = (names, values) => {
|
|
145
|
+
const i = names.findIndex((n) => n.type === 'identifier' && n.text === name);
|
|
146
|
+
if (i < 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return codeChildren(values)[i] ?? null;
|
|
149
|
+
};
|
|
150
|
+
switch (stmt.type) {
|
|
151
|
+
case 'short_var_declaration':
|
|
152
|
+
case 'assignment_statement':
|
|
153
|
+
return pick(codeChildren(stmt.childForFieldName('left')), stmt.childForFieldName('right'));
|
|
154
|
+
case 'var_spec':
|
|
155
|
+
return pick(stmt.childrenForFieldName('name'), stmt.childForFieldName('value'));
|
|
156
|
+
case 'var_declaration': {
|
|
157
|
+
const specs = stmt.namedChildren.flatMap((c) => c.type === 'var_spec_list' ? c.namedChildren : [c]);
|
|
158
|
+
for (const spec of specs) {
|
|
159
|
+
if (spec.type !== 'var_spec')
|
|
160
|
+
continue;
|
|
161
|
+
const value = pick(spec.childrenForFieldName('name'), spec.childForFieldName('value'));
|
|
162
|
+
if (value !== undefined)
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A binding whose value cannot be established statically: an earlier
|
|
173
|
+
* statement writes the name inside a nested scope (`{ g = r.Group("/new") }`,
|
|
174
|
+
* a branch or loop body), so which value reaches the use depends on control
|
|
175
|
+
* flow. Callers decline the route instead of picking the older binding.
|
|
176
|
+
*/
|
|
177
|
+
const CONFLICT = Symbol('conflicting-binding');
|
|
178
|
+
function isParamBinding(b) {
|
|
179
|
+
return typeof b === 'object' && b !== null && 'param' in b;
|
|
180
|
+
}
|
|
181
|
+
/** The parameter_declaration of `fn` (parameters or method receiver) declaring `name`. */
|
|
182
|
+
function paramDeclaring(fn, name) {
|
|
183
|
+
for (const list of [fn.childForFieldName('parameters'), fn.childForFieldName('receiver')]) {
|
|
184
|
+
for (const decl of codeChildren(list)) {
|
|
185
|
+
if (decl.type !== 'parameter_declaration' && decl.type !== 'variadic_parameter_declaration') {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (decl.childrenForFieldName('name').some((n) => n.text === name))
|
|
189
|
+
return decl;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/** Whether `inner` lies within `outer`'s source span. */
|
|
195
|
+
function within(outer, inner) {
|
|
196
|
+
return !!outer && outer.startIndex <= inner.startIndex && inner.endIndex <= outer.endIndex;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether `stmt` writes `name` with a plain assignment somewhere inside it
|
|
200
|
+
* (`g = …` in a nested block, branch, loop, or closure body). A nested `:=`
|
|
201
|
+
* declares a new variable and is not a write to the outer one.
|
|
202
|
+
*/
|
|
203
|
+
function writesNameInside(stmt, name) {
|
|
204
|
+
return [stmt, ...stmt.descendantsOfType('assignment_statement')].some((a) => a.type === 'assignment_statement' && declaresName(a.childForFieldName('left'), name));
|
|
205
|
+
}
|
|
206
|
+
/** Whether an identifier or expression_list (e.g. a range left side) declares `name`. */
|
|
207
|
+
function declaresName(node, name) {
|
|
208
|
+
if (!node)
|
|
209
|
+
return false;
|
|
210
|
+
if (node.type === 'identifier')
|
|
211
|
+
return node.text === name;
|
|
212
|
+
return node.namedChildren.some((n) => n.type === 'identifier' && n.text === name);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The value last bound to identifier `ident` before its use: the nearest
|
|
216
|
+
* binding site in the enclosing scopes, walking outward — preceding statements
|
|
217
|
+
* in blocks and switch/select cases (`expression_case`/`type_case`/
|
|
218
|
+
* `communication_case`/`default_case` act as statement containers), then
|
|
219
|
+
* statement-scoped bindings (`if`/`switch` initializers, `for` clauses
|
|
220
|
+
* including `range`, type-switch guards), up to the enclosing function
|
|
221
|
+
* declaration. Returns null when the name is a parameter, is bound without a
|
|
222
|
+
* value, is received from a channel by a select case head (the received value
|
|
223
|
+
* is statically unknown, so the walk stops instead of escaping to an outer
|
|
224
|
+
* group), or is not bound in scope.
|
|
225
|
+
*/
|
|
226
|
+
function findBinding(ident) {
|
|
227
|
+
const binding = lookupBinding(ident);
|
|
228
|
+
return isParamBinding(binding) ? null : (binding ?? null);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* findBinding's walk, keeping "declared without a traceable value" (null: a
|
|
232
|
+
* `func` literal parameter, `var x T`, a select receive) apart from "not
|
|
233
|
+
* declared before reaching the enclosing function declaration" (undefined).
|
|
234
|
+
* CONFLICT means a preceding statement writes the name in a nested scope, so
|
|
235
|
+
* the value at the use is control-flow dependent. A parameter or method
|
|
236
|
+
* receiver of the enclosing function returns its declaration (ParamBinding):
|
|
237
|
+
* no value, but a static type.
|
|
238
|
+
*/
|
|
239
|
+
function lookupBinding(ident) {
|
|
240
|
+
const name = ident.text;
|
|
241
|
+
let child = ident;
|
|
242
|
+
for (let node = ident.parent; node; child = node, node = node.parent) {
|
|
243
|
+
if (node.type === 'function_declaration' ||
|
|
244
|
+
node.type === 'method_declaration' ||
|
|
245
|
+
node.type === 'func_literal') {
|
|
246
|
+
const param = paramDeclaring(node, name);
|
|
247
|
+
if (param)
|
|
248
|
+
return { param };
|
|
249
|
+
if (node.type === 'func_literal')
|
|
250
|
+
continue;
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
// Inside a grouped `var ( a = …; b = a.Group(…) )`, the specs before the
|
|
254
|
+
// one holding the use are already in scope; the current and later specs
|
|
255
|
+
// are not (`var g = g.Group(…)` reads the outer g).
|
|
256
|
+
if (node.type === 'var_spec_list') {
|
|
257
|
+
const specs = codeChildren(node);
|
|
258
|
+
const useIndex = specs.findIndex((s) => s.id === child.id);
|
|
259
|
+
for (const spec of specs.slice(0, useIndex).reverse()) {
|
|
260
|
+
const value = boundValue(spec, name);
|
|
261
|
+
if (value !== undefined)
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (node.type === 'block' ||
|
|
267
|
+
node.type === 'expression_case' ||
|
|
268
|
+
node.type === 'type_case' ||
|
|
269
|
+
node.type === 'communication_case' ||
|
|
270
|
+
node.type === 'default_case') {
|
|
271
|
+
// A select case head can rebind the name (`case g := <-ch:` or
|
|
272
|
+
// `case g = <-ch:`); the received value is statically unknown, so the
|
|
273
|
+
// walk must STOP with no traceable value — the same decline the
|
|
274
|
+
// ingestion-side route bindings record (value null) — instead of
|
|
275
|
+
// escaping to an outer group of the same name.
|
|
276
|
+
if (node.type === 'communication_case') {
|
|
277
|
+
for (const head of node.children) {
|
|
278
|
+
if (head.type === 'receive_statement' &&
|
|
279
|
+
declaresName(head.childForFieldName('left'), name)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const stmts = codeChildren(node);
|
|
285
|
+
const useIndex = stmts.findIndex((s) => s.id === child.id);
|
|
286
|
+
for (const stmt of stmts.slice(0, useIndex).reverse()) {
|
|
287
|
+
const value = boundValue(stmt, name);
|
|
288
|
+
if (value !== undefined)
|
|
289
|
+
return value;
|
|
290
|
+
// A write nested in an earlier statement (block, branch, loop) may or
|
|
291
|
+
// may not run before the use: the reaching value is unprovable.
|
|
292
|
+
if (writesNameInside(stmt, name))
|
|
293
|
+
return CONFLICT;
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Statement-scoped bindings enclose the use the same way Go scopes them.
|
|
298
|
+
if (node.type === 'if_statement' || node.type === 'expression_switch_statement') {
|
|
299
|
+
// A use inside the initializer itself (`if g := g.Group(…); …`) reads
|
|
300
|
+
// the OUTER binding: the new one only scopes over what follows it.
|
|
301
|
+
const init = node.childForFieldName('initializer');
|
|
302
|
+
if (init && !within(init, ident)) {
|
|
303
|
+
const value = boundValue(init, name);
|
|
304
|
+
if (value !== undefined)
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (node.type === 'for_statement') {
|
|
310
|
+
const clause = codeChildren(node)[0];
|
|
311
|
+
// A write in the loop's post statement, condition, or body runs between
|
|
312
|
+
// iterations, so from the second pass on the body sees that value
|
|
313
|
+
// instead of the one it entered with (`for ; c; g = r.Group("/post")`):
|
|
314
|
+
// the prefix is control-flow dependent, so decline it.
|
|
315
|
+
if (within(node.childForFieldName('body'), ident)) {
|
|
316
|
+
const loopParts = [
|
|
317
|
+
node.childForFieldName('body'),
|
|
318
|
+
clause?.type === 'for_clause' ? clause.childForFieldName('update') : null,
|
|
319
|
+
clause?.type === 'for_clause' ? clause.childForFieldName('condition') : null,
|
|
320
|
+
];
|
|
321
|
+
if (loopParts.some((part) => part && writesNameInside(part, name)))
|
|
322
|
+
return CONFLICT;
|
|
323
|
+
}
|
|
324
|
+
if (clause?.type === 'for_clause') {
|
|
325
|
+
// Only the initializer binds before the body; an absent initializer
|
|
326
|
+
// (`for ; c; i++`) binds nothing.
|
|
327
|
+
const init = clause.childForFieldName('initializer');
|
|
328
|
+
if (init && !within(init, ident)) {
|
|
329
|
+
const value = boundValue(init, name);
|
|
330
|
+
if (value !== undefined)
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (clause?.type === 'range_clause') {
|
|
335
|
+
if (declaresName(clause.childForFieldName('left'), name) &&
|
|
336
|
+
!within(clause.childForFieldName('right'), ident)) {
|
|
337
|
+
return clause.childForFieldName('right') ?? null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (node.type === 'type_switch_statement') {
|
|
343
|
+
// `switch g := x.(type)` — the guard list is the `alias` field, which
|
|
344
|
+
// only the `:=` form has (bare `switch x.(type)` parses with none),
|
|
345
|
+
// matching how the ingestion-side route-bindings read it. The switched
|
|
346
|
+
// value is the operand right after the guard list.
|
|
347
|
+
const guard = node.childForFieldName('alias');
|
|
348
|
+
const switched = codeChildren(node)[1] ?? null;
|
|
349
|
+
if (guard?.type === 'expression_list' &&
|
|
350
|
+
declaresName(guard, name) &&
|
|
351
|
+
!within(switched, ident)) {
|
|
352
|
+
return switched;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Whether a mixed-import file's route receiver traces back to echo's
|
|
361
|
+
* constructor — `e := echo.New()` / `echo.Default()` with `echo` resolving to
|
|
362
|
+
* one of the file's verified echo import aliases — either directly or through
|
|
363
|
+
* enclosing `Group(...)` calls (`users := api.Group(…)` ← `api := e.Group(…)`
|
|
364
|
+
* ← `echo.New()`), the normal shape of grouped routes (review #7, #10).
|
|
365
|
+
* A parameter or method receiver counts by its declared type instead:
|
|
366
|
+
* `e *echo.Echo` / `g *echo.Group` (with `echo` one of those aliases) proves
|
|
367
|
+
* echo; any other type — gin's, or one the file cannot tie to echo — does not.
|
|
368
|
+
* Unrelated packages' `New()`, a local that shadows the echo import name, and
|
|
369
|
+
* anything else return false so the caller keeps the conservative
|
|
370
|
+
* last-argument fallback instead of guessing. Returns null when the Group chain exceeds MAX_GROUP_DEPTH or a
|
|
371
|
+
* binding on it is control-flow dependent (CONFLICT): the framework is then
|
|
372
|
+
* unprovable either way, so the caller declines the route.
|
|
373
|
+
*/
|
|
374
|
+
function receiverBindsToEchoConstructor(receiver, echoAliases, depth = 0) {
|
|
375
|
+
if (depth > MAX_GROUP_DEPTH)
|
|
376
|
+
return null;
|
|
377
|
+
// An identifier resolves through its binding; a chained `X.Group(…).Group(…)`
|
|
378
|
+
// operand is already a call and is inspected as-is.
|
|
379
|
+
const value = receiver.type === 'identifier' ? lookupBinding(receiver) : receiver;
|
|
380
|
+
if (value === CONFLICT)
|
|
381
|
+
return null;
|
|
382
|
+
if (isParamBinding(value))
|
|
383
|
+
return isEchoRouterType(value.param.childForFieldName('type'), echoAliases);
|
|
384
|
+
if (value?.type !== 'call_expression')
|
|
385
|
+
return false;
|
|
386
|
+
const fn = value.childForFieldName('function');
|
|
387
|
+
if (fn?.type !== 'selector_expression')
|
|
388
|
+
return false;
|
|
389
|
+
const field = fn.childForFieldName('field')?.text;
|
|
390
|
+
const operand = fn.childForFieldName('operand');
|
|
391
|
+
if (!operand)
|
|
392
|
+
return false;
|
|
393
|
+
if (field === 'New' || field === 'Default') {
|
|
394
|
+
return operand.type === 'identifier' && echoAliases.has(operand.text) && !isLocalName(operand);
|
|
395
|
+
}
|
|
396
|
+
if (field === 'Group') {
|
|
397
|
+
return receiverBindsToEchoConstructor(operand, echoAliases, depth + 1);
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
/** `*echo.Echo` / `echo.Echo` / `*echo.Group` with `echo` a verified echo import alias. */
|
|
402
|
+
function isEchoRouterType(type, echoAliases) {
|
|
403
|
+
const named = type?.type === 'pointer_type' ? codeChildren(type)[0] : type;
|
|
404
|
+
if (named?.type !== 'qualified_type')
|
|
405
|
+
return false;
|
|
406
|
+
const pkg = named.childForFieldName('package')?.text;
|
|
407
|
+
const typeName = named.childForFieldName('name')?.text;
|
|
408
|
+
return !!pkg && echoAliases.has(pkg) && (typeName === 'Echo' || typeName === 'Group');
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Whether `ident` names a local value rather than an imported package: a
|
|
412
|
+
* declaration in scope (with or without a value — `var echo Factory` shadows
|
|
413
|
+
* too) or a parameter/receiver of an enclosing function. Go lets either
|
|
414
|
+
* shadow a package qualifier (`func f(echo *Factory) { echo.New() }`).
|
|
415
|
+
*/
|
|
416
|
+
function isLocalName(ident) {
|
|
417
|
+
// lookupBinding covers parameters and receivers too (ParamBinding).
|
|
418
|
+
return lookupBinding(ident) !== undefined;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Joined `Group(...)` prefix of a route receiver; '' when it cannot be traced.
|
|
422
|
+
* Returns null when the chain exceeds MAX_GROUP_DEPTH, or when a binding on it
|
|
423
|
+
* is control-flow dependent (CONFLICT): a partial or stale prefix would emit
|
|
424
|
+
* a wrong path, so the caller declines the route instead.
|
|
425
|
+
*/
|
|
426
|
+
function groupPrefix(receiver, depth = 0) {
|
|
427
|
+
if (depth > MAX_GROUP_DEPTH)
|
|
428
|
+
return null;
|
|
429
|
+
const value = receiver.type === 'identifier' ? findBinding(receiver) : receiver;
|
|
430
|
+
if (value === CONFLICT)
|
|
431
|
+
return null;
|
|
432
|
+
const group = value ? asGroupCall(value) : null;
|
|
433
|
+
if (!group)
|
|
434
|
+
return '';
|
|
435
|
+
const outer = groupPrefix(group.parent, depth + 1);
|
|
436
|
+
return outer === null ? null : joinRoutePath(outer, group.prefix);
|
|
437
|
+
}
|
|
35
438
|
// ─── Provider: net/http `http.HandleFunc("/p", handler)` ─────────────
|
|
36
439
|
const HANDLE_FUNC_PATTERNS = compilePatterns({
|
|
37
440
|
name: 'go-handle-func',
|
|
@@ -120,29 +523,96 @@ export const GO_HTTP_PLUGIN = {
|
|
|
120
523
|
language: Go,
|
|
121
524
|
scan(tree) {
|
|
122
525
|
const out = [];
|
|
123
|
-
// Framework providers: r.GET/POST/...
|
|
526
|
+
// Framework providers: r.GET/POST/... on an engine or (nested) route group
|
|
527
|
+
const imports = readFrameworkImports(tree.rootNode);
|
|
528
|
+
const echoOnly = imports.echo.size > 0 && imports.gin.size === 0;
|
|
529
|
+
const mixed = imports.echo.size > 0 && imports.gin.size > 0;
|
|
530
|
+
// Handler names declared more than once in this file (`(h *A) List` and
|
|
531
|
+
// `(o *B) List`): the emitted name is field-only, so these must resolve
|
|
532
|
+
// only when unique in the file instead of taking the first same-named row.
|
|
533
|
+
const declaredNames = new Map();
|
|
534
|
+
for (const decl of tree.rootNode.descendantsOfType([
|
|
535
|
+
'function_declaration',
|
|
536
|
+
'method_declaration',
|
|
537
|
+
])) {
|
|
538
|
+
const declName = decl.childForFieldName('name')?.text;
|
|
539
|
+
if (declName)
|
|
540
|
+
declaredNames.set(declName, (declaredNames.get(declName) ?? 0) + 1);
|
|
541
|
+
}
|
|
124
542
|
for (const match of runCompiledPatterns(FRAMEWORK_ROUTE_PATTERNS, tree)) {
|
|
125
543
|
const methodNode = match.captures.http_method;
|
|
126
544
|
const pathNode = match.captures.path;
|
|
127
|
-
const
|
|
545
|
+
const receiverNode = match.captures.receiver;
|
|
128
546
|
if (!methodNode || !pathNode)
|
|
129
547
|
continue;
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
548
|
+
const literalPath = stringLiteral(pathNode);
|
|
549
|
+
if (literalPath === null)
|
|
550
|
+
continue;
|
|
551
|
+
const argList = pathNode.parent;
|
|
552
|
+
if (argList?.type !== 'argument_list')
|
|
553
|
+
continue;
|
|
554
|
+
const args = codeChildren(argList);
|
|
555
|
+
if (args[0]?.id !== pathNode.id)
|
|
556
|
+
continue;
|
|
557
|
+
// The path is the first code argument, so everything after it is a handler or
|
|
558
|
+
// middleware candidate: echo's verb calls take the FIRST of those, gin's
|
|
559
|
+
// the LAST (see FRAMEWORK_ROUTE_PATTERNS / readFrameworkImports). The
|
|
560
|
+
// rule is chosen per call: an echo-only file is unambiguous; a
|
|
561
|
+
// mixed-import file takes the first argument only when the receiver
|
|
562
|
+
// provably traces to echo's constructor (directly or through enclosing
|
|
563
|
+
// Group() calls) — everything else keeps the last-argument anchor,
|
|
564
|
+
// gin's order and the safer default when the file proves nothing.
|
|
565
|
+
const rest = args.slice(1);
|
|
566
|
+
if (rest.length === 0)
|
|
132
567
|
continue;
|
|
568
|
+
const echoOrder = mixed
|
|
569
|
+
? receiverNode
|
|
570
|
+
? receiverBindsToEchoConstructor(receiverNode, imports.echo)
|
|
571
|
+
: false
|
|
572
|
+
: echoOnly;
|
|
573
|
+
// A Group chain deeper than MAX_GROUP_DEPTH proves neither the full
|
|
574
|
+
// prefix nor the framework order: decline rather than emit a guess.
|
|
575
|
+
if (echoOrder === null)
|
|
576
|
+
continue;
|
|
577
|
+
const prefix = receiverNode ? groupPrefix(receiverNode) : '';
|
|
578
|
+
if (prefix === null)
|
|
579
|
+
continue;
|
|
580
|
+
const handlerNode = echoOrder ? rest[0] : rest[rest.length - 1];
|
|
581
|
+
if (!HANDLER_ARG_TYPES.has(handlerNode.type))
|
|
582
|
+
continue;
|
|
583
|
+
const path = receiverNode ? joinRoutePath(prefix, literalPath) : literalPath;
|
|
133
584
|
// An inline `func(){…}` handler has no name → emit `name: null` and a
|
|
134
585
|
// `line` so it resolves to its containing/closure symbol by line-span
|
|
135
|
-
// containment (like a consumer). A named
|
|
136
|
-
//
|
|
586
|
+
// containment (like a consumer). A named handler keeps its name and
|
|
587
|
+
// resolves by name; `line` is harmless there. For a method value or a
|
|
588
|
+
// package-qualified function (`h.List`, `pkg.List`) that name is the
|
|
589
|
+
// field, and the operand (usually a local variable, not the receiver
|
|
590
|
+
// type) does not prove where `List` is declared — so the detection is
|
|
591
|
+
// marked qualifiedHandler: resolve only to a repo-wide unique `List`,
|
|
592
|
+
// never to a same-named local method that merely shares the name.
|
|
137
593
|
const isInlineHandler = handlerNode?.type === 'func_literal';
|
|
594
|
+
const isQualified = handlerNode?.type === 'selector_expression';
|
|
595
|
+
const handlerName = isQualified
|
|
596
|
+
? (handlerNode.childForFieldName('field')?.text ?? null)
|
|
597
|
+
: (handlerNode?.text ?? null);
|
|
138
598
|
out.push({
|
|
139
599
|
role: 'provider',
|
|
140
600
|
framework: 'go-framework',
|
|
141
601
|
method: methodNode.text.toUpperCase(),
|
|
142
602
|
path,
|
|
143
|
-
name: isInlineHandler ? null :
|
|
603
|
+
name: isInlineHandler ? null : handlerName,
|
|
144
604
|
line: (handlerNode ?? pathNode).startPosition.row + 1,
|
|
145
605
|
confidence: 0.8,
|
|
606
|
+
...(isQualified ? { qualifiedHandler: true } : {}),
|
|
607
|
+
// A bare name declared more than once in this file (a function and a
|
|
608
|
+
// method of the same name) resolves only when the file holds exactly
|
|
609
|
+
// one match, rather than binding to whichever row the graph lists first.
|
|
610
|
+
...(!isQualified &&
|
|
611
|
+
!isInlineHandler &&
|
|
612
|
+
handlerName &&
|
|
613
|
+
(declaredNames.get(handlerName) ?? 0) > 1
|
|
614
|
+
? { strictHandlerResolution: true }
|
|
615
|
+
: {}),
|
|
146
616
|
});
|
|
147
617
|
}
|
|
148
618
|
// net/http HandleFunc: default method GET
|
|
@@ -151,7 +621,7 @@ export const GO_HTTP_PLUGIN = {
|
|
|
151
621
|
const handlerNode = match.captures.handler;
|
|
152
622
|
if (!pathNode)
|
|
153
623
|
continue;
|
|
154
|
-
const path =
|
|
624
|
+
const path = stringLiteral(pathNode);
|
|
155
625
|
if (path === null)
|
|
156
626
|
continue;
|
|
157
627
|
// Inline `func(){…}` handler → resolve by containment (see go-framework
|
|
@@ -176,7 +646,7 @@ export const GO_HTTP_PLUGIN = {
|
|
|
176
646
|
const httpMethod = HTTP_CLIENT_METHOD_TO_HTTP[fnNode.text];
|
|
177
647
|
if (!httpMethod)
|
|
178
648
|
continue;
|
|
179
|
-
const path =
|
|
649
|
+
const path = stringLiteral(pathNode);
|
|
180
650
|
if (path === null)
|
|
181
651
|
continue;
|
|
182
652
|
out.push({
|
|
@@ -195,8 +665,8 @@ export const GO_HTTP_PLUGIN = {
|
|
|
195
665
|
const pathNode = match.captures.path;
|
|
196
666
|
if (!methodNode || !pathNode)
|
|
197
667
|
continue;
|
|
198
|
-
const method =
|
|
199
|
-
const path =
|
|
668
|
+
const method = stringLiteral(methodNode);
|
|
669
|
+
const path = stringLiteral(pathNode);
|
|
200
670
|
if (method === null || path === null)
|
|
201
671
|
continue;
|
|
202
672
|
out.push({
|
|
@@ -215,7 +685,7 @@ export const GO_HTTP_PLUGIN = {
|
|
|
215
685
|
const pathNode = match.captures.path;
|
|
216
686
|
if (!methodNode || !pathNode)
|
|
217
687
|
continue;
|
|
218
|
-
const path =
|
|
688
|
+
const path = stringLiteral(pathNode);
|
|
219
689
|
if (path === null)
|
|
220
690
|
continue;
|
|
221
691
|
out.push({
|
|
@@ -58,6 +58,14 @@ export interface HttpDetection {
|
|
|
58
58
|
};
|
|
59
59
|
/** Resolve only from the registration file or exact import target; never guess repo-wide. */
|
|
60
60
|
strictHandlerResolution?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The handler was designated through a qualifier the plugin cannot tie to a
|
|
63
|
+
* declaration (`recv.name`, `pkg.name`): `name` alone does not prove the
|
|
64
|
+
* handler lives in the registration file. Skip the file-scoped name lookup
|
|
65
|
+
* — a same-named but unrelated local symbol would win it — and accept only
|
|
66
|
+
* a repo-wide unique match, else keep the file-level fallback.
|
|
67
|
+
*/
|
|
68
|
+
qualifiedHandler?: boolean;
|
|
61
69
|
/**
|
|
62
70
|
* The plugin saw a provider handler designator but could not prove its owner.
|
|
63
71
|
* Prevents the orchestrator from treating it as an anonymous inline handler
|
|
@@ -580,13 +580,19 @@ export class HttpRouteExtractor {
|
|
|
580
580
|
return null;
|
|
581
581
|
return resolveSymbolByNameUnique(d.handlerImport.name);
|
|
582
582
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
583
|
+
// A qualified designator (`recv.name`) does not prove the handler is
|
|
584
|
+
// declared in this file, so the file-first rung could bind an
|
|
585
|
+
// unrelated same-named local symbol: go straight to the unique
|
|
586
|
+
// repo-wide match.
|
|
587
|
+
if (!d.qualifiedHandler) {
|
|
588
|
+
const byName = d.strictHandlerResolution
|
|
589
|
+
? resolveFileSymbolByNameUnique(syms, d.name)
|
|
590
|
+
: resolveSymbolByName(syms, d.name);
|
|
591
|
+
if (byName)
|
|
592
|
+
return byName;
|
|
593
|
+
if (d.strictHandlerResolution)
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
590
596
|
const byGlobal = await resolveSymbolByNameUnique(d.name);
|
|
591
597
|
if (byGlobal)
|
|
592
598
|
return byGlobal;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { goImportPackageName } from '../languages/go/import-package-name.js';
|
|
26
26
|
import { GoRouteBindings } from '../languages/go/route-bindings.js';
|
|
27
|
+
import { stringLiteral } from './go-shared.js';
|
|
27
28
|
import { normalizeExtractedRoutePath } from './route-path.js';
|
|
28
29
|
export const GIN_ROUTE_SOURCE = 'gin-route';
|
|
29
30
|
export const ECHO_ROUTE_SOURCE = 'echo-route';
|
|
@@ -38,68 +39,6 @@ const VERBS = new Set([
|
|
|
38
39
|
]);
|
|
39
40
|
const FUNCTION_TYPE_LIST = ['function_declaration', 'method_declaration', 'func_literal'];
|
|
40
41
|
const FUNCTION_TYPES = new Set(FUNCTION_TYPE_LIST);
|
|
41
|
-
function stringLiteral(node) {
|
|
42
|
-
if (!node || node.hasError)
|
|
43
|
-
return null;
|
|
44
|
-
const body = node.text.slice(1, -1);
|
|
45
|
-
// Go discards carriage returns in raw strings, including CRLF source files.
|
|
46
|
-
if (node.type === 'raw_string_literal')
|
|
47
|
-
return body.replace(/\r/g, '');
|
|
48
|
-
if (node.type !== 'interpreted_string_literal')
|
|
49
|
-
return null;
|
|
50
|
-
if (!body.includes('\\'))
|
|
51
|
-
return body;
|
|
52
|
-
const simple = {
|
|
53
|
-
a: '\x07',
|
|
54
|
-
b: '\b',
|
|
55
|
-
f: '\f',
|
|
56
|
-
n: '\n',
|
|
57
|
-
r: '\r',
|
|
58
|
-
t: '\t',
|
|
59
|
-
v: '\v',
|
|
60
|
-
'\\': '\\',
|
|
61
|
-
'"': '"',
|
|
62
|
-
};
|
|
63
|
-
const chunks = [];
|
|
64
|
-
const tokens = /\\(?:[abfnrtv\\"]|[0-7]{3}|x[\da-fA-F]{2}|u[\da-fA-F]{4}|U[\da-fA-F]{8})|[^\\"\n]+/g;
|
|
65
|
-
let consumed = 0;
|
|
66
|
-
for (const match of body.matchAll(tokens)) {
|
|
67
|
-
if (match.index !== consumed)
|
|
68
|
-
return null;
|
|
69
|
-
const token = match[0];
|
|
70
|
-
consumed += token.length;
|
|
71
|
-
if (!token.startsWith('\\')) {
|
|
72
|
-
chunks.push(Buffer.from(token));
|
|
73
|
-
}
|
|
74
|
-
else if (simple[token[1]] !== undefined) {
|
|
75
|
-
chunks.push(Buffer.from(simple[token[1]]));
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
const octal = /[0-7]/.test(token[1]);
|
|
79
|
-
const value = Number.parseInt(token.slice(octal ? 1 : 2), octal ? 8 : 16);
|
|
80
|
-
if (octal || token[1] === 'x') {
|
|
81
|
-
// Octal and hex escapes encode bytes, not Unicode code points.
|
|
82
|
-
if (value > 255)
|
|
83
|
-
return null;
|
|
84
|
-
chunks.push(Buffer.from([value]));
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
if (value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff))
|
|
88
|
-
return null;
|
|
89
|
-
chunks.push(Buffer.from(String.fromCodePoint(value)));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (consumed !== body.length)
|
|
94
|
-
return null;
|
|
95
|
-
try {
|
|
96
|
-
// Arbitrary non-UTF-8 Go byte strings cannot be represented losslessly in a URL.
|
|
97
|
-
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
42
|
/** The framework this file routes with, when exactly one is imported. */
|
|
104
43
|
function readImports(root) {
|
|
105
44
|
let gin = null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SyntaxNode } from 'tree-sitter';
|
|
2
|
+
/**
|
|
3
|
+
* Go string-literal decoding shared by the Go route extractors on both
|
|
4
|
+
* layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
|
|
5
|
+
* group-mode HTTP plugin (`group/extractors/http-patterns/go.ts`,
|
|
6
|
+
* Strategy B). Both sides must produce the SAME text for a route literal,
|
|
7
|
+
* or the same route lands under two different contract ids that never
|
|
8
|
+
* collide in the merge — a wrong-path duplicate that survives alongside
|
|
9
|
+
* the graph's correct entry.
|
|
10
|
+
*
|
|
11
|
+
* Go's semantics, per `strconv.Unquote`:
|
|
12
|
+
* - interpreted (`"…"`) strings decode escapes (`\n`, `\x2f`, `ሴ`,
|
|
13
|
+
* `\101`, …); hex and octal escapes encode BYTES, not code points;
|
|
14
|
+
* - raw (`` `…` ``) strings have no escapes (a backslash is literal),
|
|
15
|
+
* and Go discards carriage returns in them, including CRLF source files;
|
|
16
|
+
* - a string that cannot be decoded to valid UTF-8 text (invalid escape,
|
|
17
|
+
* non-UTF-8 bytes) returns null — callers decline instead of guessing,
|
|
18
|
+
* since a URL cannot carry those bytes losslessly.
|
|
19
|
+
*
|
|
20
|
+
* Nodes that are not string literals (an identifier prefix, a rune
|
|
21
|
+
* literal, an errored subtree) also return null.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stringLiteral(node: SyntaxNode | null | undefined): string | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go string-literal decoding shared by the Go route extractors on both
|
|
3
|
+
* layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
|
|
4
|
+
* group-mode HTTP plugin (`group/extractors/http-patterns/go.ts`,
|
|
5
|
+
* Strategy B). Both sides must produce the SAME text for a route literal,
|
|
6
|
+
* or the same route lands under two different contract ids that never
|
|
7
|
+
* collide in the merge — a wrong-path duplicate that survives alongside
|
|
8
|
+
* the graph's correct entry.
|
|
9
|
+
*
|
|
10
|
+
* Go's semantics, per `strconv.Unquote`:
|
|
11
|
+
* - interpreted (`"…"`) strings decode escapes (`\n`, `\x2f`, `ሴ`,
|
|
12
|
+
* `\101`, …); hex and octal escapes encode BYTES, not code points;
|
|
13
|
+
* - raw (`` `…` ``) strings have no escapes (a backslash is literal),
|
|
14
|
+
* and Go discards carriage returns in them, including CRLF source files;
|
|
15
|
+
* - a string that cannot be decoded to valid UTF-8 text (invalid escape,
|
|
16
|
+
* non-UTF-8 bytes) returns null — callers decline instead of guessing,
|
|
17
|
+
* since a URL cannot carry those bytes losslessly.
|
|
18
|
+
*
|
|
19
|
+
* Nodes that are not string literals (an identifier prefix, a rune
|
|
20
|
+
* literal, an errored subtree) also return null.
|
|
21
|
+
*/
|
|
22
|
+
export function stringLiteral(node) {
|
|
23
|
+
if (!node || node.hasError)
|
|
24
|
+
return null;
|
|
25
|
+
const body = node.text.slice(1, -1);
|
|
26
|
+
// Go discards carriage returns in raw strings, including CRLF source files.
|
|
27
|
+
if (node.type === 'raw_string_literal')
|
|
28
|
+
return body.replace(/\r/g, '');
|
|
29
|
+
if (node.type !== 'interpreted_string_literal')
|
|
30
|
+
return null;
|
|
31
|
+
if (!body.includes('\\'))
|
|
32
|
+
return body;
|
|
33
|
+
const simple = {
|
|
34
|
+
a: '\x07',
|
|
35
|
+
b: '\b',
|
|
36
|
+
f: '\f',
|
|
37
|
+
n: '\n',
|
|
38
|
+
r: '\r',
|
|
39
|
+
t: '\t',
|
|
40
|
+
v: '\v',
|
|
41
|
+
'\\': '\\',
|
|
42
|
+
'"': '"',
|
|
43
|
+
};
|
|
44
|
+
const chunks = [];
|
|
45
|
+
const tokens = /\\(?:[abfnrtv\\"]|[0-7]{3}|x[\da-fA-F]{2}|u[\da-fA-F]{4}|U[\da-fA-F]{8})|[^\\"\n]+/g;
|
|
46
|
+
let consumed = 0;
|
|
47
|
+
for (const match of body.matchAll(tokens)) {
|
|
48
|
+
if (match.index !== consumed)
|
|
49
|
+
return null;
|
|
50
|
+
const token = match[0];
|
|
51
|
+
consumed += token.length;
|
|
52
|
+
if (!token.startsWith('\\')) {
|
|
53
|
+
chunks.push(Buffer.from(token));
|
|
54
|
+
}
|
|
55
|
+
else if (simple[token[1]] !== undefined) {
|
|
56
|
+
chunks.push(Buffer.from(simple[token[1]]));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const octal = /[0-7]/.test(token[1]);
|
|
60
|
+
const value = Number.parseInt(token.slice(octal ? 1 : 2), octal ? 8 : 16);
|
|
61
|
+
if (octal || token[1] === 'x') {
|
|
62
|
+
// Octal and hex escapes encode bytes, not Unicode code points.
|
|
63
|
+
if (value > 255)
|
|
64
|
+
return null;
|
|
65
|
+
chunks.push(Buffer.from([value]));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff))
|
|
69
|
+
return null;
|
|
70
|
+
chunks.push(Buffer.from(String.fromCodePoint(value)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (consumed !== body.length)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
// Arbitrary non-UTF-8 Go byte strings cannot be represented losslessly in a URL.
|
|
78
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.13-rc.
|
|
3
|
+
"version": "1.6.13-rc.76",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"serve": "tsx src/cli/index.ts serve",
|
|
53
53
|
"dev": "tsx watch src/cli/index.ts",
|
|
54
54
|
"test": "vitest run",
|
|
55
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
55
56
|
"test:unit": "vitest run test/unit",
|
|
56
57
|
"pretest:integration": "node scripts/build.js",
|
|
57
58
|
"test:integration": "vitest run test/integration",
|