/** * `resolveGoRouteHandler` (#3402): gin/echo handler designators resolved across * the files of a Go package, unique-or-decline at every step. * * The model is built the way the parse phase leaves it: detached Methods have * a receiver-qualified name, with ownership deferred to semantic resolution. */ import { describe, expect, it } from 'vitest'; import { createSemanticModel } from '../../src/core/ingestion/model/index.js'; import { resolveGoRouteHandler } from '../../src/core/ingestion/languages/go/route-handler.js'; import type { ExtractedDecoratorRoute } from '../../src/core/ingestion/workers/parse-worker.js'; const ROUTER = 'app/router/router.go'; type Model = ReturnType; const struct = (model: Model, file: string, name: string) => model.symbols.add(file, name, `Struct:${file}:${name}`, 'Struct'); /** A method as the worker registers it before workspace ownership resolves. */ const method = (model: Model, file: string, receiver: string, name: string) => model.symbols.add(file, name, `Method:${file}:${receiver}.${name}`, 'Method', { qualifiedName: `${receiver}.${name}`, }); const fn = (model: Model, file: string, name: string, returnType?: string) => model.symbols.add(file, name, `Function:${file}:${name}`, 'Function', { returnType }); const route = (overrides: Partial): ExtractedDecoratorRoute => ({ filePath: ROUTER, routePath: '/x', httpMethod: 'GET', decoratorName: 'GET', lineNumber: 1, source: 'gin-route', ...overrides, }); const resolve = ( model: Model, overrides: Partial, imports: Readonly> = {}, ) => resolveGoRouteHandler(route(overrides), { model, importTargetsFor: (_from, localName) => imports[localName] ?? [], }); describe('resolveGoRouteHandler', () => { it('deduplicates a method indexed by both its canonical owner and receiver name', () => { const model = createSemanticModel(); struct(model, 'app/router/h.go', 'H'); model.symbols.add('app/router/h.go', 'Do', 'Method:app/router/h.go:H.Do', 'Method', { ownerId: 'Struct:app/router/h.go:H', qualifiedName: 'H.Do', }); const context = { model, importTargetsFor: () => [] }; const registration = route({ handlerName: 'h.Do', handlerReceiver: { kind: 'type', name: 'H' }, }); expect(resolveGoRouteHandler(registration, context)).toBe('Method:app/router/h.go:H.Do'); expect(resolveGoRouteHandler(registration, context)).toBe('Method:app/router/h.go:H.Do'); }); it('rebuilds package indexes for a new resolution pass over a changed model', () => { const model = createSemanticModel(); struct(model, 'app/router/types.go', 'H'); method(model, 'app/router/h.go', 'H', 'Do'); const registration = { handlerName: 'h.Do', handlerReceiver: { kind: 'type', name: 'H' }, } as const; expect(resolve(model, registration)).toBe('Method:app/router/h.go:H.Do'); method(model, 'app/router/other.go', 'H', 'Do'); expect(resolve(model, registration)).toBeUndefined(); }); it('resolves a constructor-built receiver to a method declared in a sibling file (issue #3402)', () => { const model = createSemanticModel(); struct(model, 'app/router/types.go', 'MatchHandler'); fn(model, 'app/router/types.go', 'NewMatchHandler', '*MatchHandler'); method(model, 'app/router/match_handler.go', 'MatchHandler', 'UnfinalizeRoundHandle'); expect( resolve(model, { handlerName: 'matchHandler.UnfinalizeRoundHandle', handlerReceiver: { kind: 'constructor', name: 'NewMatchHandler' }, }), ).toBe('Method:app/router/match_handler.go:MatchHandler.UnfinalizeRoundHandle'); }); it('reads the first result of a (T, error) constructor', () => { const model = createSemanticModel(); struct(model, 'app/router/h.go', 'H'); fn(model, 'app/router/h.go', 'NewH', '(*H, error)'); method(model, 'app/router/h.go', 'H', 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'constructor', name: 'NewH' }, }), ).toBe('Method:app/router/h.go:H.Do'); }); it('declines a constructor whose result type lives in another package', () => { const model = createSemanticModel(); fn(model, 'app/router/h.go', 'NewH', '*other.H'); method(model, 'app/router/h.go', 'H', 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'constructor', name: 'NewH' }, }), ).toBeUndefined(); }); describe.each(['Händler', '处理器', '𐐀Handler', '_Händler_١'])( 'a constructor returning the Go identifier %s', (owner) => { it.each(['%s', '*%s', '(*%s, error)'])( 'resolves its sibling-file method for result %s', (result) => { const model = createSemanticModel(); struct(model, 'app/router/types.go', owner); fn(model, 'app/router/constructor.go', 'NewHandler', result.replace('%s', owner)); method(model, 'app/router/handler.go', owner, 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'constructor', name: 'NewHandler' }, }), ).toBe(`Method:app/router/handler.go:${owner}.Do`); }, ); }, ); it.each([ '1Handler', '١Handler', 'Ha\u0308ndler', 'Handler²', 'HandlerⅣ', 'other.Handler', '[]Handler', 'map[string]Handler', ])('declines the unsupported constructor result %s even with matching metadata', (result) => { const model = createSemanticModel(); struct(model, 'app/router/types.go', result); fn(model, 'app/router/constructor.go', 'NewHandler', result); method(model, 'app/router/handler.go', result, 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'constructor', name: 'NewHandler' }, }), ).toBeUndefined(); }); it('resolves a type hint through an import qualifier', () => { const model = createSemanticModel(); struct(model, 'app/handlers/auth.go', 'Auth'); method(model, 'app/handlers/auth.go', 'Auth', 'Login'); expect( resolve( model, { handlerName: 'h.Login', handlerReceiver: { kind: 'type', name: 'Auth', qualifier: 'hs' }, }, { hs: ['app/handlers/auth.go', 'app/handlers/util.go'] }, ), ).toBe('Method:app/handlers/auth.go:Auth.Login'); }); it('resolves a package function through its import', () => { const model = createSemanticModel(); fn(model, 'app/handlers/health.go', 'Health'); fn(model, 'app/other/health.go', 'Health'); expect( resolve( model, { handlerName: 'handlers.Health', handlerReceiver: { kind: 'module', qualifier: 'handlers' }, }, { handlers: ['app/handlers/health.go'] }, ), ).toBe('Function:app/handlers/health.go:Health'); }); it('declines a module handler whose import is not in the workspace', () => { const model = createSemanticModel(); fn(model, 'app/router/x.go', 'Wrap'); expect( resolve(model, { handlerName: 'gin.Wrap', handlerReceiver: { kind: 'module', qualifier: 'gin' }, }), ).toBeUndefined(); }); it('resolves a bare function in the route package only', () => { const model = createSemanticModel(); fn(model, 'app/router/health.go', 'Health'); fn(model, 'app/other/health.go', 'Health'); expect(resolve(model, { handlerName: 'Health' })).toBe('Function:app/router/health.go:Health'); }); it('declines a receiver of unknown type even when the name is unique in the router package', () => { // `h := deps.Users` — the real handler usually lives in another package, so a // same-named method in the router's own directory belongs to an unrelated type. const model = createSemanticModel(); method(model, 'app/router/page.go', 'Page', 'List'); expect(resolve(model, { handlerName: 'h.List' })).toBeUndefined(); }); it('ignores _test.go siblings when judging uniqueness', () => { const model = createSemanticModel(); struct(model, 'app/router/auth.go', 'AuthHandler'); method(model, 'app/router/auth.go', 'AuthHandler', 'Login'); method(model, 'app/router/auth_test.go', 'AuthHandler', 'Login'); expect( resolve(model, { handlerName: 'h.Login', handlerReceiver: { kind: 'type', name: 'AuthHandler' }, }), ).toBe('Method:app/router/auth.go:AuthHandler.Login'); }); it.each([ ['a qualifier whose import spans two directories', { kind: 'module', qualifier: 'split' }], ['a type hint with no name', { kind: 'type' }], ] as const)('declines %s', (_label, handlerReceiver) => { const model = createSemanticModel(); fn(model, 'app/a/h.go', 'Do'); method(model, 'app/router/t.go', 'T', 'Do'); expect( resolve( model, { handlerName: 'h.Do', handlerReceiver }, { split: ['app/a/h.go', 'app/b/h.go'] }, ), ).toBeUndefined(); }); it('declines a deeper selector', () => { const model = createSemanticModel(); method(model, 'app/router/t.go', 'T', 'Do'); expect(resolve(model, { handlerName: 'a.b.Do' })).toBeUndefined(); }); it('declines when two same-named constructors exist in the package', () => { const model = createSemanticModel(); struct(model, 'app/router/h.go', 'H'); fn(model, 'app/router/h.go', 'NewH', '*H'); fn(model, 'app/router/h2.go', 'NewH', '*H'); method(model, 'app/router/h.go', 'H', 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'constructor', name: 'NewH' }, }), ).toBeUndefined(); }); describe('a failed hint never falls back to a name-only match', () => { it.each([ ['the hinted type is not in the package', () => {}], [ 'two structs share the hinted name', (model: Model) => { struct(model, 'app/router/t1.go', 'T'); struct(model, 'app/router/t2.go', 'T'); }, ], [ 'the hinted type has no such method', (model: Model) => { struct(model, 'app/router/t.go', 'T'); }, ], ])('declines when %s', (_label, arrange) => { const model = createSemanticModel(); arrange(model); method(model, 'app/router/other.go', 'Other', 'Do'); expect( resolve(model, { handlerName: 'h.Do', handlerReceiver: { kind: 'type', name: 'T' } }), ).toBeUndefined(); }); }); });