diff --git a/gitnexus/src/core/group/extractors/http-patterns/go.ts b/gitnexus/src/core/group/extractors/http-patterns/go.ts index a36a236b0..e4a89ff37 100644 --- a/gitnexus/src/core/group/extractors/http-patterns/go.ts +++ b/gitnexus/src/core/group/extractors/http-patterns/go.ts @@ -199,10 +199,21 @@ function declaresName(node: Parser.SyntaxNode | null, name: string): boolean { * group), or is not bound in scope. */ function findBinding(ident: Parser.SyntaxNode): Parser.SyntaxNode | null { + return lookupBinding(ident) ?? null; +} + +/** + * findBinding's walk, keeping "declared without a traceable value" (null: a + * `func` literal parameter, `var x T`, a select receive) apart from "not + * declared before reaching the enclosing function declaration" (undefined). + */ +function lookupBinding(ident: Parser.SyntaxNode): Parser.SyntaxNode | null | undefined { const name = ident.text; let child: Parser.SyntaxNode = ident; for (let node = ident.parent; node; child = node, node = node.parent) { - if (node.type === 'function_declaration' || node.type === 'method_declaration') return null; + if (node.type === 'function_declaration' || node.type === 'method_declaration') { + return undefined; + } if (node.type === 'func_literal') { const params = node.childForFieldName('parameters')?.descendantsOfType('identifier') ?? []; if (params.some((p) => p.text === name)) return null; @@ -278,7 +289,7 @@ function findBinding(ident: Parser.SyntaxNode): Parser.SyntaxNode | null { continue; } } - return null; + return undefined; } /** @@ -319,11 +330,12 @@ function receiverBindsToEchoConstructor( /** * Whether `ident` names a local value rather than an imported package: a - * binding in scope or a parameter/receiver of an enclosing function. Go lets - * either shadow a package qualifier (`func f(echo *Factory) { echo.New() }`). + * declaration in scope (with or without a value — `var echo Factory` shadows + * too) or a parameter/receiver of an enclosing function. Go lets either + * shadow a package qualifier (`func f(echo *Factory) { echo.New() }`). */ function isLocalName(ident: Parser.SyntaxNode): boolean { - if (findBinding(ident) !== null) return true; + if (lookupBinding(ident) !== undefined) return true; for (let node = ident.parent; node; node = node.parent) { if ( node.type !== 'func_literal' && 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 0285dec42..e6fe6b658 100644 --- a/gitnexus/test/unit/group/go-gin-route-groups.test.ts +++ b/gitnexus/test/unit/group/go-gin-route-groups.test.ts @@ -596,6 +596,25 @@ func routes(echo *Factory) { ).toEqual([{ method: 'GET', path: '/x', name: 'Middleware' }]); }); + it('does not mistake a value-less local declaration of echo for the import', () => { + // `var echo Factory` declares a local without an initializer; it still + // shadows the package qualifier, so `echo.New()` proves nothing. + expect( + providers(`package main +import ( + "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" +) + +func routes() { + var echo Factory + e := echo.New() + e.GET("/x", h.Handler, auth.Middleware) +} +`), + ).toEqual([{ method: 'GET', path: '/x', name: 'Middleware' }]); + }); + it('declines a route whose Group chain exceeds the depth cap', () => { // Past MAX_GROUP_DEPTH (32) the full prefix — and, in a mixed file, the // framework order — is unprovable: emitting the outer prefixes alone (or