From dd1ed19c6e9a5825f5b731a2dec48aa3ce0217dc Mon Sep 17 00:00:00 2001 From: rgb-vgx Date: Sun, 4 Oct 2026 13:13:09 +0700 Subject: [PATCH] fix(group): trace grouped receivers to their framework constructor Review finding on the mixed-import handler choice: a route registered on a group (`api := e.Group(...); api.GET(...)`) bound its receiver to the Group() call rather than a constructor, so constructor provenance failed and the conservative last-argument fallback picked echo's middleware as the handler name. receiverBindsToEchoConstructor now walks enclosing Group() calls on the way to the constructor (depth-capped by MAX_GROUP_DEPTH, mirroring groupPrefix), so an echo group chain resolves to echo.New()/echo.Default() and takes the first-argument handler, while a gin chain still fails the echo-alias check and keeps the last-argument rule. Provenance must still END at a constructor: parameters and unrelated New() remain fallback. Tests: 39 in the go-gin file (new: grouped echo + gin chains in one mixed file); group suite 1302 green; prettier/eslint/tsc clean. Co-Authored-By: Claude Code --- .../core/group/extractors/http-patterns/go.ts | 48 +++++++++++-------- .../unit/group/go-gin-route-groups.test.ts | 27 +++++++++++ 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/gitnexus/src/core/group/extractors/http-patterns/go.ts b/gitnexus/src/core/group/extractors/http-patterns/go.ts index a06b0f4dc..bda4e30f8 100644 --- a/gitnexus/src/core/group/extractors/http-patterns/go.ts +++ b/gitnexus/src/core/group/extractors/http-patterns/go.ts @@ -282,27 +282,37 @@ function findBinding(ident: Parser.SyntaxNode): Parser.SyntaxNode | null { } /** - * Whether a mixed-import file's route receiver is DIRECTLY bound to echo's - * constructor — `e := echo.New()` / `e := echo.Default()` with `echo` - * resolving to one of the file's verified echo import aliases (review #7). - * Only a direct constructor binding proves which framework's argument order - * the call follows: parameters, receivers reached through `Group(...)` or - * other calls, and unrelated packages' `New()` all return false so the caller - * keeps the conservative last-argument fallback instead of guessing. + * Whether a mixed-import file's route receiver traces back to echo's + * constructor — `e := echo.New()` / `echo.Default()` with `echo` resolving to + * one of the file's verified echo import aliases — either directly or through + * enclosing `Group(...)` calls (`users := api.Group(…)` ← `api := e.Group(…)` + * ← `echo.New()`), the normal shape of grouped routes (review #7, #10). + * Provenance must still END at a constructor: parameters, unrelated packages' + * `New()`, and anything else return false so the caller keeps the + * conservative last-argument fallback instead of guessing. */ function receiverBindsToEchoConstructor( receiver: Parser.SyntaxNode, echoAliases: ReadonlySet, + depth = 0, ): boolean { - if (receiver.type !== 'identifier') return false; - const binding = findBinding(receiver); - if (binding?.type !== 'call_expression') return false; - const fn = binding.childForFieldName('function'); + if (depth > MAX_GROUP_DEPTH) return false; + // An identifier resolves through its binding; a chained `X.Group(…).Group(…)` + // operand is already a call and is inspected as-is. + const value = receiver.type === 'identifier' ? findBinding(receiver) : receiver; + if (value?.type !== 'call_expression') return false; + const fn = value.childForFieldName('function'); if (fn?.type !== 'selector_expression') return false; - const ctor = fn.childForFieldName('field')?.text; - if (ctor !== 'New' && ctor !== 'Default') return false; - const pkg = fn.childForFieldName('operand'); - return pkg?.type === 'identifier' && echoAliases.has(pkg.text); + const field = fn.childForFieldName('field')?.text; + const operand = fn.childForFieldName('operand'); + if (!operand) return false; + if (field === 'New' || field === 'Default') { + return operand.type === 'identifier' && echoAliases.has(operand.text); + } + if (field === 'Group') { + return receiverBindsToEchoConstructor(operand, echoAliases, depth + 1); + } + return false; } /** Joined `Group(...)` prefix of a route receiver; '' when it cannot be traced. */ @@ -425,10 +435,10 @@ export const GO_HTTP_PLUGIN: HttpLanguagePlugin = { // middleware candidate: echo's verb calls take the FIRST of those, gin's // the LAST (see FRAMEWORK_ROUTE_PATTERNS / readFrameworkImports). The // rule is chosen per call: an echo-only file is unambiguous; a - // mixed-import file takes the first argument only when the receiver is - // provably bound to echo's constructor — everything else keeps the - // last-argument anchor, gin's order and the safer default when the - // file proves nothing. + // mixed-import file takes the first argument only when the receiver + // provably traces to echo's constructor (directly or through enclosing + // Group() calls) — everything else keeps the last-argument anchor, + // gin's order and the safer default when the file proves nothing. const rest = argList.namedChildren.slice(1); if (rest.length === 0) continue; const echoOrder = mixed diff --git a/gitnexus/test/unit/group/go-gin-route-groups.test.ts b/gitnexus/test/unit/group/go-gin-route-groups.test.ts index f8362ca4d..2a7bfaf30 100644 --- a/gitnexus/test/unit/group/go-gin-route-groups.test.ts +++ b/gitnexus/test/unit/group/go-gin-route-groups.test.ts @@ -575,6 +575,33 @@ func routes() { ).toEqual([{ method: 'GET', path: '/x', name: 'Middleware' }]); }); + it('traces a grouped receiver back to its framework constructor in a mixed file', () => { + // The normal grouped shape: `users := api.Group(…)` ← `api := e.Group(…)` + // ← `echo.New()` proves echo order through the Group chain, while the + // gin chain resolves to gin.Default() and keeps the last-argument rule. + expect( + providers(`package main +import ( + "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" +) + +func routes() { + e := echo.New() + api := e.Group("/api") + users := api.Group("/users") + users.GET("/:id", h.Handler, auth.Middleware) + r := gin.Default() + v1 := r.Group("/v1") + v1.POST("/x", auth.Middleware, h.Post) +} +`), + ).toEqual([ + { method: 'GET', path: '/api/users/:id', name: 'Handler' }, + { method: 'POST', path: '/v1/x', name: 'Post' }, + ]); + }); + it('applies the same group logic to echo', () => { expect( providers(`package main