import { bundle } from '../project'; import { describe, expect, it } from 'vitest'; import { run, imports } from '../compile'; import { ProtobufCompiler } from '../../../src/compiler/compiler'; const box = 'interface Box {value:pb<1,T>;}'; const codec = `${box} class Codec { encode(value:Box) {return protobuf_encode>(value);} decode(bytes:Uint8Array) {return protobuf_decode>(bytes);} }`; describe('generic class instances', () => { it('isolates uint32 and string instances across alternating calls', () => { const result = run(`${codec} const numbers=new Codec(); const strings=new Codec(); export const result=Array.from({length:32},(_,i)=>[numbers.decode(numbers.encode({value:i})),strings.decode(strings.encode({value:'v'+i}))]); export const sharedMethod=numbers.encode===strings.encode; export const keys=Object.keys(numbers); `); expect(result.result).toEqual( Array.from({ length: 32 }, (_, i) => [{ value: i }, { value: 'v' + i }]), ); expect(result.sharedMethod).toBe(true); expect(result.keys).toEqual([]); }); it('emits one codec per concrete type for many instances', () => { const declarations = Array.from( { length: 32 }, (_, i) => `const n${i}=new Codec(); const s${i}=new Codec();`, ).join('\n'); const output = new ProtobufCompiler().compile(imports + codec + declarations, 'reuse.ts')!; expect(output.match(/function protobuf_encode_Box__uint_32\(/g)).toHaveLength(1); expect(output.match(/function protobuf_decode_Box__uint_32\(/g)).toHaveLength(1); expect(output.match(/function protobuf_encode_Box__string\(/g)).toHaveLength(1); expect(output.match(/function protobuf_decode_Box__string\(/g)).toHaveLength(1); }); it('alternates method type arguments on one instance', () => { expect( run(`${box} class Codec {roundtrip(value:Box) {return protobuf_decode>(protobuf_encode>(value));}} const instance=new Codec(); export const result=[instance.roundtrip({value:7}),instance.roundtrip({value:'x'}),instance.roundtrip({value:8})]; `).result, ).toEqual([{ value: 7 }, { value: 'x' }, { value: 8 }]); }); it('forwards outer generics through factories returning class instances', () => { expect( run(`${codec} function create() {return new Codec();} const n=create(); const s=create(); export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))]; `).result, ).toEqual([{ value: 7 }, { value: 'x' }]); }); it('combines factory class arguments and independent method arguments', () => { expect( run(`interface Pair {first:pb<1,T>;second:pb<2,U>;} class Codec {roundtrip(value:Pair) {return protobuf_decode>(protobuf_encode>(value));}} function create():Codec {return new Codec();} const n=create(); const s=create(); const alias=n; export const result=[n.roundtrip({first:7,second:'x'}),s.roundtrip({first:'y',second:8}),alias.roundtrip({first:9,second:10}),create().roundtrip({first:'a',second:'b'})]; `).result, ).toEqual([ { first: 7, second: 'x' }, { first: 'y', second: 8 }, { first: 9, second: 10 }, { first: 'a', second: 'b' }, ]); }); it('infers constructor generics from annotated arguments', () => { expect( run(`${box} class Codec {constructor(readonly value:Box) {} encode(){return protobuf_encode>(this.value);}} const n:Box={value:7}; const s:Box={value:'x'}; const numbers=new Codec(n); const strings=new Codec(s); export const result=[[...numbers.encode()],[...strings.encode()]]; `).result, ).toEqual([ [8, 7], [10, 1, 120], ]); }); it('infers factories and inherited constructors with renamed parameters', () => { expect( run(`${box} class Codec {constructor(readonly value:Box) {} encode(){return protobuf_encode>(this.value);}} class Middle extends Codec {} class Derived extends Middle {} function create(value:Box) {return new Derived(value);} const n:Box={value:7}; const s:Box={value:'x'}; const numbers=create(n); const strings=create(s); export const result=[[...numbers.encode()],[...strings.encode()]]; `).result, ).toEqual([ [8, 7], [10, 1, 120], ]); }); it('specializes unannotated factories, aliases and default class arguments', () => { expect( run(`interface Pair {first:pb<1,T>;second:pb<2,U>;} class Codec {roundtrip(value:Pair) {return protobuf_decode>(protobuf_encode>(value));}} const create=()=>new Codec(); type Alias = Codec; function use(instance:Alias, first:T) {return instance.roundtrip({first,second:'ok'});} export const result=[create().roundtrip({first:7,second:'x'}),new Codec().roundtrip({first:'y',second:8}),use(create(),'z')]; `).result, ).toEqual([ { first: 7, second: 'x' }, { first: 'y', second: 8 }, { first: 'z', second: 'ok' }, ]); }); it('retains class bindings in generic callback adapters', () => { expect( run(`interface Pair {first:pb<1,T>;second:pb<2,U>;} class Codec {roundtrip(value:Pair) {return protobuf_decode>(protobuf_encode>(value));}} function create():Codec {return new Codec();} const numbers=create(); const strings=create(); const n=numbers.roundtrip.bind(numbers); const s=strings.roundtrip.bind(strings); export const result=[n({first:7,second:'x'}),s({first:'y',second:8})]; `).result, ).toEqual([ { first: 7, second: 'x' }, { first: 'y', second: 8 }, ]); }); it('separates specializations through inherited constructors', () => { expect( run(`${codec} class Derived extends Codec {} const n=new Derived(); const s=new Derived(); export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))]; `).result, ).toEqual([{ value: 7 }, { value: 'x' }]); }); it('keeps cross-file factory instances specialized', async () => { expect( await bundle({ 'codec.ts': `${imports} ${codec} export function create() {return new Codec();}`, 'entry.ts': `import {create} from './codec'; const n=create();const s=create(); export const result=[n.decode(n.encode({value:7})),s.decode(s.encode({value:'x'}))];`, }), ).toEqual({ result: [{ value: 7 }, { value: 'x' }] }); }); it('resolves method defaults from the class type argument', () => { expect( run(`interface Pair {first:pb<1,T>;second:pb<2,U>;} class Codec {roundtrip(value:Pair) {return protobuf_decode>(protobuf_encode>(value));}} function create():Codec {return new Codec();} export const result=[create().roundtrip({first:7,second:8}),create().roundtrip({first:'x',second:'y'})]; `).result, ).toEqual([ { first: 7, second: 8 }, { first: 'x', second: 'y' }, ]); }); it('specializes inherited static generic methods', () => { expect( run(`${box} class Base {static roundtrip(value:Box) {return protobuf_decode>(protobuf_encode>(value));}} class Codec extends Base {} export const result=[Codec.roundtrip({value:7}),Codec.roundtrip({value:'x'})]; `).result, ).toEqual([{ value: 7 }, { value: 'x' }]); }); it('combines cross-file class and method generics', async () => { expect( await bundle({ 'codec.ts': `${imports} export interface Pair {first:pb<1,T>;second:pb<2,U>;} class Codec {roundtrip(value:Pair) {return protobuf_decode>(protobuf_encode>(value));}} export const create=()=>new Codec();`, 'entry.ts': `import {create} from './codec'; const n=create(); const s=create(); export const result=[n.roundtrip({first:7,second:'x'}),s.roundtrip({first:'y',second:8})];`, }), ).toEqual({ result: [ { first: 7, second: 'x' }, { first: 'y', second: 8 }, ], }); }); });