From 9d6f52dcdd745fd6bc52505e4abf9f33cfbb484f Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sun, 4 Oct 2026 23:11:39 +0300 Subject: [PATCH] [TS] Lock the rest-parameter caller packing contract (#373) --- .../ets/test/EtsTsArrayParameterTest.kt | 11 ++++++ jacodb-ets/ts-frontend/src/dto/signatures.ts | 1 + .../ts-frontend/test/rest-parameters.spec.ts | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 jacodb-ets/ts-frontend/test/rest-parameters.spec.ts diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsArrayParameterTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsArrayParameterTest.kt index f145df058..3a7865815 100644 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsArrayParameterTest.kt +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsArrayParameterTest.kt @@ -73,4 +73,15 @@ class EtsTsArrayParameterTest { assertTrue(method.cfg.stmts.any { it is EtsIfStmt }) } + + @Test + fun `rest parameter metadata survives frontend JSON and model conversion`() { + val dto = roundTrip("export function count(...values: number[]): number { return values.length; }") + + val dtoMethod = dto.classes.flatMap { it.methods }.single { it.signature.name == "count" } + val method = dto.toEtsFile().classes.flatMap { it.methods }.single { it.name == "count" } + + assertTrue(dtoMethod.signature.parameters.single().isRest) + assertTrue(method.signature.parameters.single().isRest) + } } diff --git a/jacodb-ets/ts-frontend/src/dto/signatures.ts b/jacodb-ets/ts-frontend/src/dto/signatures.ts index be55104e0..dcdaa02a8 100644 --- a/jacodb-ets/ts-frontend/src/dto/signatures.ts +++ b/jacodb-ets/ts-frontend/src/dto/signatures.ts @@ -52,6 +52,7 @@ export interface MethodParameterDto { name: string; type: TypeDto; isOptional?: boolean; // Kotlin default: false + /** Call consumers pack trailing raw arguments into the array read by ParameterRef. */ isRest?: boolean; // Kotlin default: false } diff --git a/jacodb-ets/ts-frontend/test/rest-parameters.spec.ts b/jacodb-ets/ts-frontend/test/rest-parameters.spec.ts new file mode 100644 index 000000000..6c2d6af53 --- /dev/null +++ b/jacodb-ets/ts-frontend/test/rest-parameters.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { consumers } from "./array-test-consumers"; +import { lower, methodByName } from "./util"; +import { serializeEtsFile } from "../src/serialize"; + +describe("rest parameter caller contract", () => { + it("keeps isRest through JSON and uses it for calls with zero and multiple raw arguments", () => { + const source = "function count(...values: number[]): number { return values.length; }"; + const { file, diagnostics } = lower(source); + const roundTripped = JSON.parse(serializeEtsFile(file)); + const method = methodByName(roundTripped, "count"); + const { ir, native } = consumers(source, "count"); + + expect(diagnostics.messages).toEqual([]); + expect(method.signature.parameters).toHaveLength(1); + expect(method.signature.parameters[0].isRest).toBe(true); + for (const args of [[], [1], [1, 2]]) { + expect(ir(...args)).toBe(native(...args)); + expect(ir(...args)).toBe(args.length); + } + }); + + it("passes raw arguments through frontend calls to the packing consumer", () => { + const source = "function count(first: number, ...rest: number[]) { return rest.length; } function invoke() { return count(1, 2, 3); }"; + const { ir, native } = consumers(source, "invoke"); + + expect(ir()).toBe(native()); + expect(ir()).toBe(2); + }); + + it("packs remaining raw arguments in order, including missing normal arguments", () => { + const { ir, native } = consumers("function f(first: number, ...rest: number[]) { return rest; }", "f"); + + for (const args of [[], [1], [1, 2], [1, 2, 3]]) { + expect(ir(...args)).toEqual(native(...args)); + } + }); +});