gitnexus 1.6.13-rc.77 → 1.6.13-rc.79
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/augmentation/engine.js +8 -6
- package/dist/core/group/extractors/http-patterns/go.js +223 -24
- package/dist/core/ingestion/route-extractors/go-gin-echo.js +3 -3
- package/dist/core/ingestion/route-extractors/go-shared.d.ts +8 -0
- package/dist/core/ingestion/route-extractors/go-shared.js +12 -0
- package/package.json +1 -1
|
@@ -108,7 +108,7 @@ export async function augment(pattern, cwd) {
|
|
|
108
108
|
await initLbug(repoId, repo.lbugPath);
|
|
109
109
|
}
|
|
110
110
|
// Step 1: BM25 search (fast, no embeddings)
|
|
111
|
-
const { results: bm25Results
|
|
111
|
+
const { results: bm25Results } = await searchFTSFromLbug(pattern, 10, repoId);
|
|
112
112
|
// Step 2: Map BM25 file results to symbols
|
|
113
113
|
const symbolMatches = [];
|
|
114
114
|
for (const result of bm25Results.slice(0, 5)) {
|
|
@@ -135,14 +135,16 @@ export async function augment(pattern, cwd) {
|
|
|
135
135
|
/* skip */
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
138
|
+
// FTS ranks files by mentions, so a widely referenced symbol's definition
|
|
139
|
+
// may not be present in the top file results. Fall back to graph names
|
|
140
|
+
// whenever those files produce no symbol match, regardless of FTS health.
|
|
141
|
+
if (symbolMatches.length === 0) {
|
|
141
142
|
const fallbackRows = await executeQuery(repoId, `
|
|
142
143
|
MATCH (n)
|
|
143
144
|
WHERE n.name CONTAINS '${patternFirstWord}'
|
|
144
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
145
|
-
|
|
145
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath,
|
|
146
|
+
CASE WHEN n.name = '${patternFirstWord}' THEN 0 ELSE 1 END AS exactRank
|
|
147
|
+
ORDER BY exactRank, id
|
|
146
148
|
LIMIT 5
|
|
147
149
|
`).catch(() => []);
|
|
148
150
|
for (const sym of fallbackRows) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Go from 'tree-sitter-go';
|
|
2
2
|
import { goImportPackageName } from '../../../ingestion/languages/go/import-package-name.js';
|
|
3
|
-
import { stringLiteral } from '../../../ingestion/route-extractors/go-shared.js';
|
|
3
|
+
import { isEchoImportPath, isGinImportPath, stringLiteral, } from '../../../ingestion/route-extractors/go-shared.js';
|
|
4
4
|
import { compilePatterns, runCompiledPatterns, } from '../tree-sitter-scanner.js';
|
|
5
5
|
/**
|
|
6
6
|
* Go HTTP plugin. Handles:
|
|
@@ -54,7 +54,8 @@ const HANDLER_ARG_TYPES = new Set([
|
|
|
54
54
|
]);
|
|
55
55
|
/**
|
|
56
56
|
* The file's framework import aliases: which local qualifiers resolve to
|
|
57
|
-
* echo and to gin. Matched on the import path
|
|
57
|
+
* echo and to gin. Matched on the EXACT import path (shared with ingestion via
|
|
58
|
+
* go-shared.ts) rather than the local name, so
|
|
58
59
|
* an aliased import still counts; an unaliased import is keyed by its
|
|
59
60
|
* conventional package name (`goImportPackageName`). `_` and `.` imports bind
|
|
60
61
|
* no qualifier this file can route through. An empty set means the file
|
|
@@ -77,9 +78,9 @@ function readFrameworkImports(root) {
|
|
|
77
78
|
const local = spec.childForFieldName('name')?.text ?? goImportPackageName(importPath);
|
|
78
79
|
if (local === '_' || local === '.')
|
|
79
80
|
continue;
|
|
80
|
-
if (importPath
|
|
81
|
+
if (isEchoImportPath(importPath))
|
|
81
82
|
echo.add(local);
|
|
82
|
-
else if (importPath
|
|
83
|
+
else if (isGinImportPath(importPath))
|
|
83
84
|
gin.add(local);
|
|
84
85
|
}
|
|
85
86
|
return { echo, gin };
|
|
@@ -139,10 +140,22 @@ function asGroupCall(node) {
|
|
|
139
140
|
return null;
|
|
140
141
|
return { parent, prefix };
|
|
141
142
|
}
|
|
143
|
+
/** The `var_spec`s of a `var_declaration`, flattened out of a grouped `var (…)`. */
|
|
144
|
+
function varSpecs(decl) {
|
|
145
|
+
return decl.namedChildren
|
|
146
|
+
.flatMap((c) => (c.type === 'var_spec_list' ? c.namedChildren : [c]))
|
|
147
|
+
.filter((spec) => spec.type === 'var_spec');
|
|
148
|
+
}
|
|
142
149
|
/** The expression `name` is assigned by `stmt` (`:=`, `=`, or `var`), if any. */
|
|
143
150
|
function boundValue(stmt, name) {
|
|
144
151
|
const pick = (names, values) => {
|
|
145
|
-
|
|
152
|
+
// Go carries an assignment out left to right, so a repeated target leaves
|
|
153
|
+
// the LAST one in place (`g, g = a, b` ends with g == b).
|
|
154
|
+
let i = -1;
|
|
155
|
+
for (let k = 0; k < names.length; k++) {
|
|
156
|
+
if (names[k].type === 'identifier' && names[k].text === name)
|
|
157
|
+
i = k;
|
|
158
|
+
}
|
|
146
159
|
if (i < 0)
|
|
147
160
|
return undefined;
|
|
148
161
|
return codeChildren(values)[i] ?? null;
|
|
@@ -154,10 +167,7 @@ function boundValue(stmt, name) {
|
|
|
154
167
|
case 'var_spec':
|
|
155
168
|
return pick(stmt.childrenForFieldName('name'), stmt.childForFieldName('value'));
|
|
156
169
|
case 'var_declaration': {
|
|
157
|
-
const
|
|
158
|
-
for (const spec of specs) {
|
|
159
|
-
if (spec.type !== 'var_spec')
|
|
160
|
-
continue;
|
|
170
|
+
for (const spec of varSpecs(stmt)) {
|
|
161
171
|
const value = pick(spec.childrenForFieldName('name'), spec.childForFieldName('value'));
|
|
162
172
|
if (value !== undefined)
|
|
163
173
|
return value;
|
|
@@ -196,20 +206,200 @@ function within(outer, inner) {
|
|
|
196
206
|
return !!outer && outer.startIndex <= inner.startIndex && inner.endIndex <= outer.endIndex;
|
|
197
207
|
}
|
|
198
208
|
/**
|
|
199
|
-
* Whether `
|
|
200
|
-
* (`g = …`
|
|
201
|
-
*
|
|
209
|
+
* Whether `node` is itself a write to `name`: a plain `assignment_statement`
|
|
210
|
+
* (`g = …`) or the `=` form of a range clause (`for _, g = range xs`) or a
|
|
211
|
+
* select receive (`case g = <-ch`), which tree-sitter parses as their own node
|
|
212
|
+
* types. A `:=` declares a new variable — loop- or case-local — and is not a
|
|
213
|
+
* write to the outer one.
|
|
214
|
+
*/
|
|
215
|
+
function isWriteTo(node, name) {
|
|
216
|
+
if (node.type === 'assignment_statement') {
|
|
217
|
+
return declaresName(node.childForFieldName('left'), name);
|
|
218
|
+
}
|
|
219
|
+
return ((node.type === 'range_clause' || node.type === 'receive_statement') &&
|
|
220
|
+
assignsExistingName(node) &&
|
|
221
|
+
declaresName(node.childForFieldName('left'), name));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Every write to `name` inside `scope` (a statement or a function literal).
|
|
225
|
+
* With `executedOnly`, a nested function literal is entered only through a
|
|
226
|
+
* call: one merely stored in a value does not run when its enclosing body
|
|
227
|
+
* runs.
|
|
228
|
+
*/
|
|
229
|
+
function writesInScope(scope, name, executedOnly) {
|
|
230
|
+
const out = [];
|
|
231
|
+
const walk = (node) => {
|
|
232
|
+
if (node.type === 'func_literal') {
|
|
233
|
+
if (executedOnly && !isInvokedLiteral(node))
|
|
234
|
+
return;
|
|
235
|
+
const body = node.childForFieldName('body');
|
|
236
|
+
if (body)
|
|
237
|
+
walk(body);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (isWriteTo(node, name))
|
|
241
|
+
out.push(node);
|
|
242
|
+
for (const child of node.namedChildren)
|
|
243
|
+
walk(child);
|
|
244
|
+
};
|
|
245
|
+
walk(scope);
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Whether `scope` writes `name` through a binding declared OUTSIDE it: an
|
|
250
|
+
* assignment (or `=` range/receive) whose target resolves to a name the scope
|
|
251
|
+
* itself does not declare — a captured outer group rather than a local `:=`,
|
|
252
|
+
* `var`, or parameter. An unresolvable or conflicting write reaches out
|
|
253
|
+
* conservatively. With `executedOnly`, writes that only run if a nested
|
|
254
|
+
* function literal is later called are ignored.
|
|
255
|
+
*/
|
|
256
|
+
function writesOuterName(scope, name, executedOnly = false) {
|
|
257
|
+
for (const write of writesInScope(scope, name, executedOnly)) {
|
|
258
|
+
const ident = identifierNamed(write.childForFieldName('left'), name);
|
|
259
|
+
if (!ident)
|
|
260
|
+
continue;
|
|
261
|
+
// A range/receive `=` target resolves from OUTSIDE its clause: the
|
|
262
|
+
// clause's own binding logic describes the loop/case variable, not the
|
|
263
|
+
// existing name the `=` writes.
|
|
264
|
+
const from = write.type === 'range_clause' || write.type === 'receive_statement'
|
|
265
|
+
? write.parent
|
|
266
|
+
: undefined;
|
|
267
|
+
const binding = lookupBinding(ident, from);
|
|
268
|
+
// null is a declaration without a traceable value; it carries no position
|
|
269
|
+
// to place, so a write through it reaches out conservatively.
|
|
270
|
+
if (binding === undefined || binding === null || binding === CONFLICT)
|
|
271
|
+
return true;
|
|
272
|
+
const declared = isParamBinding(binding) ? binding.param : binding;
|
|
273
|
+
if (!within(scope, declared))
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether `stmt` writes `name` through a binding declared outside it — a
|
|
280
|
+
* nested assignment, branch, loop, or closure body. A declaration that
|
|
281
|
+
* shadows `name` inside the statement (a nested `:=`, a parameter) writes the
|
|
282
|
+
* local, not the target the use would see, so it does not count.
|
|
202
283
|
*/
|
|
203
284
|
function writesNameInside(stmt, name) {
|
|
204
|
-
return
|
|
285
|
+
return writesOuterName(stmt, name);
|
|
205
286
|
}
|
|
206
|
-
/**
|
|
207
|
-
function
|
|
287
|
+
/** A range clause or receive that assigns (`=`) rather than declares (`:=`). */
|
|
288
|
+
function assignsExistingName(clause) {
|
|
289
|
+
return !clause.children.some((c) => c.type === ':=');
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Whether `fn` is the callee of a call (`func(){ … }()`), ignoring
|
|
293
|
+
* parentheses, so its body is known to run.
|
|
294
|
+
*/
|
|
295
|
+
function isInvokedLiteral(fn) {
|
|
296
|
+
let callee = fn;
|
|
297
|
+
while (callee.parent?.type === 'parenthesized_expression')
|
|
298
|
+
callee = callee.parent;
|
|
299
|
+
const parent = callee.parent;
|
|
300
|
+
return (!!parent &&
|
|
301
|
+
parent.type === 'call_expression' &&
|
|
302
|
+
parent.childForFieldName('function')?.id === callee.id);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Whether evaluating `node` executes a write to `name`. A function literal is
|
|
306
|
+
* only entered when it is the callee of a call: `g := func(){ g = … }` merely
|
|
307
|
+
* stores the literal, so the outer `g` is untouched, while `func(){ … }()`
|
|
308
|
+
* runs its body (writes that resolve to the literal's own locals excepted).
|
|
309
|
+
*/
|
|
310
|
+
function executedWritesName(node, name) {
|
|
311
|
+
if (node.type === 'func_literal') {
|
|
312
|
+
return isInvokedLiteral(node) && writesOuterName(node, name, true);
|
|
313
|
+
}
|
|
314
|
+
if (isWriteTo(node, name))
|
|
315
|
+
return true;
|
|
316
|
+
return node.namedChildren.some((child) => executedWritesName(child, name));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Whether the initializer of the declaration that brings `name` into scope in
|
|
320
|
+
* a block writes the OUTER `name`. The right-hand side runs before the new
|
|
321
|
+
* variable exists, so `g := func(){ g = … }()` still writes the binding the
|
|
322
|
+
* use outside the block would inherit — but a literal only stored in a value
|
|
323
|
+
* never runs its body. Grouped `var` specs up to and including the declaring
|
|
324
|
+
* one are scanned; later specs read the new local.
|
|
325
|
+
*/
|
|
326
|
+
function initializerWritesName(decl, name) {
|
|
327
|
+
if (decl.type === 'short_var_declaration') {
|
|
328
|
+
const values = decl.childForFieldName('right');
|
|
329
|
+
return !!values && executedWritesName(values, name);
|
|
330
|
+
}
|
|
331
|
+
if (decl.type === 'var_declaration') {
|
|
332
|
+
for (const spec of varSpecs(decl)) {
|
|
333
|
+
const value = spec.childForFieldName('value');
|
|
334
|
+
if (value && executedWritesName(value, name))
|
|
335
|
+
return true;
|
|
336
|
+
if (boundValue(spec, name) !== undefined)
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Whether `stmt` holds a function literal that writes the outer `name`. Such
|
|
344
|
+
* a literal can run after any later direct assignment (`reset := func(){ g = … };
|
|
345
|
+
* g = …; reset()`), so captured writes are found independently of statement
|
|
346
|
+
* order instead of being lost behind the last-assignment return. Writes that
|
|
347
|
+
* resolve to a declaration inside the literal (a parameter, a nested `:=`) are
|
|
348
|
+
* local and do not count.
|
|
349
|
+
*/
|
|
350
|
+
function closureWritesName(stmt, name) {
|
|
351
|
+
const literals = stmt.type === 'func_literal'
|
|
352
|
+
? [stmt, ...stmt.descendantsOfType('func_literal')]
|
|
353
|
+
: stmt.descendantsOfType('func_literal');
|
|
354
|
+
return literals.some((fn) => writesOuterName(fn, name));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The effect a bare `{ … }` block placed before the use has on the outer
|
|
358
|
+
* `name`. A bare block always runs, so its last top-level `name = v` is the
|
|
359
|
+
* value that reaches the use (`{ g = r.Group("/new") }` → `/new`); a write
|
|
360
|
+
* nested in a branch, loop, or closure inside it is CONFLICT. A `:=` or `var`
|
|
361
|
+
* of `name` in the block starts a new variable, so only the statements before
|
|
362
|
+
* it touch the outer one — but the declaring statement's initializer still
|
|
363
|
+
* does, because it evaluates before the new variable is in scope. undefined
|
|
364
|
+
* when the block leaves `name` alone.
|
|
365
|
+
*/
|
|
366
|
+
function bareBlockWrite(block, name) {
|
|
367
|
+
const stmts = codeChildren(block);
|
|
368
|
+
const declIndex = stmts.findIndex((s) => (s.type === 'short_var_declaration' || s.type === 'var_declaration') &&
|
|
369
|
+
boundValue(s, name) !== undefined);
|
|
370
|
+
const outerRegion = declIndex < 0 ? stmts : stmts.slice(0, declIndex);
|
|
371
|
+
if (declIndex >= 0 && initializerWritesName(stmts[declIndex], name))
|
|
372
|
+
return CONFLICT;
|
|
373
|
+
if (outerRegion.some((stmt) => closureWritesName(stmt, name)))
|
|
374
|
+
return CONFLICT;
|
|
375
|
+
for (const stmt of outerRegion.reverse()) {
|
|
376
|
+
if (stmt.type === 'assignment_statement') {
|
|
377
|
+
const value = boundValue(stmt, name);
|
|
378
|
+
if (value !== undefined)
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
if (stmt.type === 'block') {
|
|
382
|
+
const inner = bareBlockWrite(stmt, name);
|
|
383
|
+
if (inner !== undefined)
|
|
384
|
+
return inner;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (writesNameInside(stmt, name))
|
|
388
|
+
return CONFLICT;
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
/** The identifier named `name` on an assignment side (or null). */
|
|
393
|
+
function identifierNamed(node, name) {
|
|
208
394
|
if (!node)
|
|
209
|
-
return
|
|
395
|
+
return null;
|
|
210
396
|
if (node.type === 'identifier')
|
|
211
|
-
return node.text === name;
|
|
212
|
-
return node.namedChildren.
|
|
397
|
+
return node.text === name ? node : null;
|
|
398
|
+
return node.namedChildren.find((n) => n.type === 'identifier' && n.text === name) ?? null;
|
|
399
|
+
}
|
|
400
|
+
/** Whether an identifier or expression_list (e.g. a range left side) declares `name`. */
|
|
401
|
+
function declaresName(node, name) {
|
|
402
|
+
return identifierNamed(node, name) !== null;
|
|
213
403
|
}
|
|
214
404
|
/**
|
|
215
405
|
* The value last bound to identifier `ident` before its use: the nearest
|
|
@@ -234,12 +424,14 @@ function findBinding(ident) {
|
|
|
234
424
|
* CONFLICT means a preceding statement writes the name in a nested scope, so
|
|
235
425
|
* the value at the use is control-flow dependent. A parameter or method
|
|
236
426
|
* receiver of the enclosing function returns its declaration (ParamBinding):
|
|
237
|
-
* no value, but a static type.
|
|
427
|
+
* no value, but a static type. `from` starts the walk at that node's parent
|
|
428
|
+
* instead of the identifier's, so an `=` range/receive target can be resolved
|
|
429
|
+
* outside its own clause.
|
|
238
430
|
*/
|
|
239
|
-
function lookupBinding(ident) {
|
|
431
|
+
function lookupBinding(ident, from) {
|
|
240
432
|
const name = ident.text;
|
|
241
|
-
let child = ident;
|
|
242
|
-
for (let node =
|
|
433
|
+
let child = from ?? ident;
|
|
434
|
+
for (let node = child.parent; node; child = node, node = node.parent) {
|
|
243
435
|
if (node.type === 'function_declaration' ||
|
|
244
436
|
node.type === 'method_declaration' ||
|
|
245
437
|
node.type === 'func_literal') {
|
|
@@ -287,8 +479,15 @@ function lookupBinding(ident) {
|
|
|
287
479
|
const value = boundValue(stmt, name);
|
|
288
480
|
if (value !== undefined)
|
|
289
481
|
return value;
|
|
290
|
-
// A
|
|
291
|
-
|
|
482
|
+
// A bare block always runs: its unconditional writes are bindings.
|
|
483
|
+
if (stmt.type === 'block') {
|
|
484
|
+
const written = bareBlockWrite(stmt, name);
|
|
485
|
+
if (written !== undefined)
|
|
486
|
+
return written;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// A write nested in an earlier branch, loop, or closure may or may
|
|
490
|
+
// not run before the use: the reaching value is unprovable.
|
|
292
491
|
if (writesNameInside(stmt, name))
|
|
293
492
|
return CONFLICT;
|
|
294
493
|
}
|
|
@@ -24,7 +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
|
+
import { isEchoImportPath, isGinImportPath, stringLiteral } from './go-shared.js';
|
|
28
28
|
import { normalizeExtractedRoutePath } from './route-path.js';
|
|
29
29
|
export const GIN_ROUTE_SOURCE = 'gin-route';
|
|
30
30
|
export const ECHO_ROUTE_SOURCE = 'echo-route';
|
|
@@ -58,9 +58,9 @@ function readImports(root) {
|
|
|
58
58
|
const local = explicit ?? goImportPackageName(importPath);
|
|
59
59
|
if (!local)
|
|
60
60
|
continue;
|
|
61
|
-
if (importPath
|
|
61
|
+
if (isGinImportPath(importPath))
|
|
62
62
|
gin = local;
|
|
63
|
-
if (
|
|
63
|
+
if (isEchoImportPath(importPath))
|
|
64
64
|
echo = local;
|
|
65
65
|
}
|
|
66
66
|
// Both, or neither: no way to tell which argument is the handler.
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { SyntaxNode } from 'tree-sitter';
|
|
2
|
+
/**
|
|
3
|
+
* Framework import paths, matched EXACTLY on both layers (ingestion's
|
|
4
|
+
* `go-gin-echo.ts` and the group-mode `http-patterns/go.ts`): a substring
|
|
5
|
+
* test would classify `example.com/labstack/echo-wrapper` as echo on one
|
|
6
|
+
* side only, flipping the handler-argument order and splitting contracts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isGinImportPath(importPath: string): boolean;
|
|
9
|
+
export declare function isEchoImportPath(importPath: string): boolean;
|
|
2
10
|
/**
|
|
3
11
|
* Go string-literal decoding shared by the Go route extractors on both
|
|
4
12
|
* layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework import paths, matched EXACTLY on both layers (ingestion's
|
|
3
|
+
* `go-gin-echo.ts` and the group-mode `http-patterns/go.ts`): a substring
|
|
4
|
+
* test would classify `example.com/labstack/echo-wrapper` as echo on one
|
|
5
|
+
* side only, flipping the handler-argument order and splitting contracts.
|
|
6
|
+
*/
|
|
7
|
+
export function isGinImportPath(importPath) {
|
|
8
|
+
return importPath === 'github.com/gin-gonic/gin';
|
|
9
|
+
}
|
|
10
|
+
export function isEchoImportPath(importPath) {
|
|
11
|
+
return /^github\.com\/labstack\/echo(\/v\d+)?$/.test(importPath);
|
|
12
|
+
}
|
|
1
13
|
/**
|
|
2
14
|
* Go string-literal decoding shared by the Go route extractors on both
|
|
3
15
|
* layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
|
package/package.json
CHANGED