|
@@ -25,6 +25,7 @@ import type { Edge, Node } from '../types';
|
|
|
import type { QueryBuilder } from '../db/queries';
|
|
import type { QueryBuilder } from '../db/queries';
|
|
|
import type { ResolutionContext } from './types';
|
|
import type { ResolutionContext } from './types';
|
|
|
import { isGeneratedFile } from '../extraction/generated-detection';
|
|
import { isGeneratedFile } from '../extraction/generated-detection';
|
|
|
|
|
+import { stripCommentsForRegex } from './strip-comments';
|
|
|
|
|
|
|
|
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
|
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
@@ -966,11 +967,129 @@ function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {
|
|
|
return edges;
|
|
return edges;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Gin middleware chain. Gin runs its entire handler chain through one dynamic
|
|
|
|
|
+ * line in `(*Context).Next`:
|
|
|
|
|
+ * for c.index < len(c.handlers) { c.handlers[c.index](c); c.index++ }
|
|
|
|
|
+ * `c.handlers` is a `HandlersChain` (`[]HandlerFunc`) assembled at registration
|
|
|
|
|
+ * time by `combineHandlers` from the funcs passed to `r.Use(...)` /
|
|
|
|
|
+ * `r.GET("/path", h...)` / `r.Handle(...)`. Because the call is a computed index
|
|
|
|
|
+ * into a runtime-built slice, tree-sitter resolves `c.handlers[c.index](c)` to
|
|
|
|
|
+ * NOTHING — so `callees(Next)` is just the `len()` helper and the flow
|
|
|
|
|
+ * `ServeHTTP → handleHTTPRequest → Next` dead-ends at the exact symbol the
|
|
|
|
|
+ * "how do requests flow through the middleware chain" question is about. The
|
|
|
|
|
+ * agent then re-queries Next and falls back to Read/grep (validated: the gin
|
|
|
|
|
+ * WITH-arm rabbit-holed on precisely this dead-end).
|
|
|
|
|
+ *
|
|
|
|
|
+ * Bridge it: find the chain DISPATCHER (a Go method whose body invokes a
|
|
|
|
|
+ * `handlers` slice by index) and link it → every HandlerFunc registered via a
|
|
|
|
|
+ * gin registration call, so `callees(Next)` and `trace(ServeHTTP, <handler>)`
|
|
|
|
|
+ * connect end-to-end. Named handlers only (`gin.Logger()` → `Logger`,
|
|
|
|
|
+ * `authMiddleware`); inline closures are anonymous and skipped. Like
|
|
|
|
|
+ * react-render / interface-impl this is a deliberate over-approximation —
|
|
|
|
|
+ * reachability-correct (any registered handler CAN run for some route), capped,
|
|
|
|
|
+ * and gated on the dispatcher existing so it never runs on non-gin Go repos.
|
|
|
|
|
+ * Provenance `heuristic`, `synthesizedBy:'gin-middleware-chain'`; `registeredAt`
|
|
|
|
|
+ * is the `.Use`/`.GET` site an agent would otherwise grep for.
|
|
|
|
|
+ */
|
|
|
|
|
+const GIN_DISPATCH_RE = /\.handlers\s*\[[^\]]*\]\s*\(/; // c.handlers[c.index](c)
|
|
|
|
|
+const GIN_REG_RE = /\.(?:Use|GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\s*\(/g;
|
|
|
|
|
+
|
|
|
|
|
+/** Balanced `(...)` body starting at the '(' index; null if unbalanced. */
|
|
|
|
|
+function goBalancedArgs(s: string, openIdx: number): string | null {
|
|
|
|
|
+ let depth = 0;
|
|
|
|
|
+ for (let i = openIdx; i < s.length; i++) {
|
|
|
|
|
+ const c = s[i];
|
|
|
|
|
+ if (c === '(') depth++;
|
|
|
|
|
+ else if (c === ')') { depth--; if (depth === 0) return s.slice(openIdx + 1, i); }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+/** Split a top-level comma list, respecting nested () [] {}. */
|
|
|
|
|
+function goSplitArgs(args: string): string[] {
|
|
|
|
|
+ const out: string[] = [];
|
|
|
|
|
+ let depth = 0, cur = '';
|
|
|
|
|
+ for (const c of args) {
|
|
|
|
|
+ if (c === '(' || c === '[' || c === '{') { depth++; cur += c; }
|
|
|
|
|
+ else if (c === ')' || c === ']' || c === '}') { depth--; cur += c; }
|
|
|
|
|
+ else if (c === ',' && depth === 0) { out.push(cur); cur = ''; }
|
|
|
|
|
+ else cur += c;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cur.trim()) out.push(cur);
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+/** Tail ident of a handler arg: `gin.Logger()`→`Logger`, `mw`→`mw`; null for string paths / closures. */
|
|
|
|
|
+function goHandlerIdent(expr: string): string | null {
|
|
|
|
|
+ const cleaned = expr.trim().replace(/\(\s*\)$/, ''); // drop a trailing call ()
|
|
|
|
|
+ if (!cleaned || cleaned.startsWith('"') || cleaned.startsWith('`') || cleaned.startsWith('func')) return null;
|
|
|
|
|
+ const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
|
|
|
+ return m ? m[1]! : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
|
|
+ // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
|
|
|
+ const dispatchers = queries.getNodesByKind('method').filter((n) => {
|
|
|
|
|
+ if (n.language !== 'go') return false;
|
|
|
|
|
+ const content = ctx.readFile(n.filePath);
|
|
|
|
|
+ const src = content && sliceLines(content, n.startLine, n.endLine);
|
|
|
|
|
+ return !!src && GIN_DISPATCH_RE.test(src);
|
|
|
|
|
+ });
|
|
|
|
|
+ if (dispatchers.length === 0) return []; // not a gin repo — bail
|
|
|
|
|
+
|
|
|
|
|
+ // 2. Collect handler identifiers registered via gin registration calls
|
|
|
|
|
+ // (.Use / .GET / … / .Handle). String args (paths/methods) and inline
|
|
|
|
|
+ // closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
|
|
|
+ const registered = new Map<string, string>(); // name → registeredAt (file:line)
|
|
|
|
|
+ for (const file of ctx.getAllFiles()) {
|
|
|
|
|
+ if (!file.endsWith('.go')) continue;
|
|
|
|
|
+ const content = ctx.readFile(file);
|
|
|
|
|
+ if (!content || (!content.includes('.Use(') && !/\.(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\(/.test(content))) continue;
|
|
|
|
|
+ const safe = stripCommentsForRegex(content, 'go');
|
|
|
|
|
+ GIN_REG_RE.lastIndex = 0;
|
|
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
|
|
+ while ((m = GIN_REG_RE.exec(safe))) {
|
|
|
|
|
+ const parenIdx = m.index + m[0].length - 1;
|
|
|
|
|
+ const argStr = goBalancedArgs(safe, parenIdx);
|
|
|
|
|
+ if (!argStr) continue;
|
|
|
|
|
+ const line = safe.slice(0, m.index).split('\n').length;
|
|
|
|
|
+ for (const arg of goSplitArgs(argStr)) {
|
|
|
|
|
+ const name = goHandlerIdent(arg);
|
|
|
|
|
+ if (name && !registered.has(name)) registered.set(name, `${file}:${line}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (registered.size === 0) return [];
|
|
|
|
|
+
|
|
|
|
|
+ // 3. Link each dispatcher → each registered handler node (dedup, capped).
|
|
|
|
|
+ const edges: Edge[] = [];
|
|
|
|
|
+ const seen = new Set<string>();
|
|
|
|
|
+ for (const disp of dispatchers) {
|
|
|
|
|
+ let added = 0;
|
|
|
|
|
+ for (const [name, registeredAt] of registered) {
|
|
|
|
|
+ if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
|
|
|
|
|
+ const handler = ctx.getNodesByName(name).find(
|
|
|
|
|
+ (n) => (n.kind === 'function' || n.kind === 'method') && n.language === 'go'
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!handler || handler.id === disp.id) continue;
|
|
|
|
|
+ const key = `${disp.id}>${handler.id}`;
|
|
|
|
|
+ if (seen.has(key)) continue;
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ edges.push({
|
|
|
|
|
+ source: disp.id, target: handler.id, kind: 'calls', line: disp.startLine,
|
|
|
|
|
+ provenance: 'heuristic',
|
|
|
|
|
+ metadata: { synthesizedBy: 'gin-middleware-chain', via: name, registeredAt },
|
|
|
|
|
+ });
|
|
|
|
|
+ added++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return edges;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
|
* React re-render + JSX children + Vue templates + RN event channel +
|
|
* React re-render + JSX children + Vue templates + RN event channel +
|
|
|
- * Fabric native-impl + MyBatis Java↔XML). Returns the count added. Never
|
|
|
|
|
- * throws into indexing — callers wrap in try/catch.
|
|
|
|
|
|
|
+ * Fabric native-impl + MyBatis Java↔XML + Gin middleware chain). Returns the
|
|
|
|
|
+ * count added. Never throws into indexing — callers wrap in try/catch.
|
|
|
*/
|
|
*/
|
|
|
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
|
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
|
|
const fieldEdges = fieldChannelEdges(queries, ctx);
|
|
const fieldEdges = fieldChannelEdges(queries, ctx);
|
|
@@ -985,6 +1104,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
|
|
const rnEventEdgesList = rnEventEdges(ctx);
|
|
const rnEventEdgesList = rnEventEdges(ctx);
|
|
|
const fabricNativeEdges = fabricNativeImplEdges(ctx);
|
|
const fabricNativeEdges = fabricNativeImplEdges(ctx);
|
|
|
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
|
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
|
|
|
|
+ const ginEdges = ginMiddlewareChainEdges(queries, ctx);
|
|
|
|
|
|
|
|
const merged: Edge[] = [];
|
|
const merged: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
const seen = new Set<string>();
|
|
@@ -1001,6 +1121,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
|
|
...rnEventEdgesList,
|
|
...rnEventEdgesList,
|
|
|
...fabricNativeEdges,
|
|
...fabricNativeEdges,
|
|
|
...mybatisEdges,
|
|
...mybatisEdges,
|
|
|
|
|
+ ...ginEdges,
|
|
|
]) {
|
|
]) {
|
|
|
const key = `${e.source}>${e.target}`;
|
|
const key = `${e.source}>${e.target}`;
|
|
|
if (seen.has(key)) continue;
|
|
if (seen.has(key)) continue;
|