fix(group): treat value-less local declarations as shadowing the echo import

findBinding collapses "declared without a value" (`var echo Factory`)
and "not declared" into null, so isLocalName missed that shadow. Split
the walk into lookupBinding, which returns undefined only when the name
is not declared before the enclosing function; findBinding keeps its
null contract and isLocalName checks for any declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rgb-vgx 2026-10-04 15:33:32 +07:00
parent 7d736f71ca
commit b1aea8e956
2 changed files with 36 additions and 5 deletions

View file

@ -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' &&

View file

@ -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