From 74b60da442a060632f5a340bc9c945c1147d2d01 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sun, 4 Oct 2026 22:50:35 +0300 Subject: [PATCH] feat(ts): preserve symbol annotations and inferred types (#379) --- .../main/kotlin/org/jacodb/ets/dto/Convert.kt | 3 + .../kotlin/org/jacodb/ets/dto/Serializers.kt | 1 + .../main/kotlin/org/jacodb/ets/dto/Types.kt | 7 ++ .../main/kotlin/org/jacodb/ets/model/Type.kt | 16 ++++ .../org/jacodb/ets/test/EtsTsSymbolTest.kt | 76 +++++++++++++++++++ jacodb-ets/ts-frontend/src/dto/types.ts | 6 ++ jacodb-ets/ts-frontend/src/types/convert.ts | 4 + jacodb-ets/ts-frontend/test/types.spec.ts | 2 + 8 files changed, 115 insertions(+) create mode 100644 jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsSymbolTest.kt diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt index f7b0fcbbc..9a633674f 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt @@ -138,6 +138,7 @@ import org.jacodb.ets.model.EtsStrictNotEqExpr import org.jacodb.ets.model.EtsStringConstant import org.jacodb.ets.model.EtsStringLiteralType import org.jacodb.ets.model.EtsStringType +import org.jacodb.ets.model.EtsSymbolType import org.jacodb.ets.model.EtsSubExpr import org.jacodb.ets.model.EtsThis import org.jacodb.ets.model.EtsThrowStmt @@ -742,6 +743,8 @@ fun TypeDto.toEtsType(): EtsType = when (this) { StringTypeDto -> EtsStringType + SymbolTypeDto -> EtsSymbolType + is TupleTypeDto -> EtsTupleType( types = types.map { it.toEtsType() }, ) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt index 2e991eaa1..0351ada97 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Serializers.kt @@ -51,6 +51,7 @@ internal val valueModule = SerializersModule { internal val typeModule = SerializersModule { polymorphic(TypeDto::class) { subclass(BigIntTypeDto::class, BigIntTypeDto.serializer()) + subclass(SymbolTypeDto::class, SymbolTypeDto.serializer()) } polymorphicDefaultDeserializer(TypeDto::class) { RawTypeSerializer } } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt index b5f0bf446..d0683cf97 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Types.kt @@ -128,6 +128,13 @@ data object StringTypeDto : PrimitiveTypeDto { get() = "string" } +@Serializable +@SerialName("SymbolType") +data object SymbolTypeDto : PrimitiveTypeDto { + override val name: String + get() = "symbol" +} + @Serializable @SerialName("NullType") data object NullTypeDto : PrimitiveTypeDto { diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Type.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Type.kt index 8da58d871..8a5b8ae85 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Type.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Type.kt @@ -41,6 +41,10 @@ interface EtsType : TypeName, CommonType { error("Cannot handle ${type::class.java.simpleName}: $type") } fun visit(type: EtsStringType): R + fun visit(type: EtsSymbolType): R { + if (this is Default) return defaultVisit(type) + error("Cannot handle ${type::class.java.simpleName}: $type") + } fun visit(type: EtsNullType): R fun visit(type: EtsUndefinedType): R fun visit(type: EtsVoidType): R @@ -83,6 +87,7 @@ interface EtsType : TypeName, CommonType { override fun visit(type: EtsNumberType): R = defaultVisit(type) override fun visit(type: EtsBigIntType): R = defaultVisit(type) override fun visit(type: EtsStringType): R = defaultVisit(type) + override fun visit(type: EtsSymbolType): R = defaultVisit(type) override fun visit(type: EtsNullType): R = defaultVisit(type) override fun visit(type: EtsUndefinedType): R = defaultVisit(type) override fun visit(type: EtsVoidType): R = defaultVisit(type) @@ -261,6 +266,17 @@ object EtsStringType : EtsPrimitiveType { } } +object EtsSymbolType : EtsPrimitiveType { + override val typeName: String + get() = "symbol" + + override fun toString(): String = typeName + + override fun accept(visitor: EtsType.Visitor): R { + return visitor.visit(this) + } +} + object EtsNullType : EtsPrimitiveType { override val typeName: String get() = "null" diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsSymbolTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsSymbolTest.kt new file mode 100644 index 000000000..f79eae508 --- /dev/null +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsSymbolTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.ets.test + +import kotlin.io.path.createTempDirectory +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jacodb.ets.dto.EtsFileDto +import org.jacodb.ets.dto.dtoModule +import org.jacodb.ets.dto.toEtsFile +import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME +import org.jacodb.ets.utils.EtsIrProvider +import org.jacodb.ets.utils.generateEtsIR +import org.junit.jupiter.api.Test +import org.jacodb.ets.dto.SymbolTypeDto +import org.jacodb.ets.model.EtsSymbolType + +class EtsTsSymbolTest { + companion object { + /** Run the PRODUCTION integration path: generateEtsIR + EtsFileDto.loadFromJson. */ + private fun runFrontend(source: String, fileName: String = "test.ts"): EtsFileDto { + val dir = createTempDirectory("ts-frontend-test") + val inputPath = dir.resolve(fileName) + inputPath.writeText(source) + + val outputPath = generateEtsIR( + inputPath, + isProject = false, + timeout = null, + provider = EtsIrProvider.TS_FRONTEND, + ) + assertTrue(outputPath.exists(), "ts-frontend did not produce output: $outputPath") + + return EtsFileDto.loadFromJson(outputPath.readText()) + } + } + + @Test + fun `symbol parameter and return types survive frontend JSON and model conversion`() { + val frontendDto = runFrontend(source = "export function same(value: symbol): symbol { return value; }") + val roundTripped = EtsFileDto.loadFromJson( + Json { serializersModule = dtoModule }.encodeToString(frontendDto), + ) + val methodDto = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME } + .methods.single { it.signature.name == "same" } + + assertEquals(SymbolTypeDto, methodDto.signature.parameters.single().type) + assertEquals(SymbolTypeDto, methodDto.signature.returnType) + + val modelMethod = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME } + .methods.single { it.name == "same" } + + assertEquals(EtsSymbolType, modelMethod.signature.parameters.single().type) + assertEquals(EtsSymbolType, modelMethod.signature.returnType) + } + +} diff --git a/jacodb-ets/ts-frontend/src/dto/types.ts b/jacodb-ets/ts-frontend/src/dto/types.ts index 89c37191e..21b1dd732 100644 --- a/jacodb-ets/ts-frontend/src/dto/types.ts +++ b/jacodb-ets/ts-frontend/src/dto/types.ts @@ -42,6 +42,7 @@ export type TypeDto = | NumberTypeDto | BigIntTypeDto | StringTypeDto + | SymbolTypeDto | NullTypeDto | UndefinedTypeDto | LiteralTypeDto @@ -127,6 +128,10 @@ export interface StringTypeDto { readonly _: "StringType"; } +export interface SymbolTypeDto { + readonly _: "SymbolType"; +} + export interface NullTypeDto { readonly _: "NullType"; } @@ -185,5 +190,6 @@ export const BOOLEAN_TYPE: BooleanTypeDto = { _: "BooleanType" }; export const NUMBER_TYPE: NumberTypeDto = { _: "NumberType" }; export const BIGINT_TYPE: BigIntTypeDto = { _: "BigIntType" }; export const STRING_TYPE: StringTypeDto = { _: "StringType" }; +export const SYMBOL_TYPE: SymbolTypeDto = { _: "SymbolType" }; export const NULL_TYPE: NullTypeDto = { _: "NullType" }; export const UNDEFINED_TYPE: UndefinedTypeDto = { _: "UndefinedType" }; diff --git a/jacodb-ets/ts-frontend/src/types/convert.ts b/jacodb-ets/ts-frontend/src/types/convert.ts index 087f4f876..68bf4c137 100644 --- a/jacodb-ets/ts-frontend/src/types/convert.ts +++ b/jacodb-ets/ts-frontend/src/types/convert.ts @@ -50,6 +50,7 @@ import { NULL_TYPE, NUMBER_TYPE, STRING_TYPE, + SYMBOL_TYPE, TypeDto, UNDEFINED_TYPE, UNKNOWN_TYPE, @@ -169,6 +170,8 @@ export class TypeConverter { return BIGINT_TYPE; case ts.SyntaxKind.StringKeyword: return STRING_TYPE; + case ts.SyntaxKind.SymbolKeyword: + return SYMBOL_TYPE; case ts.SyntaxKind.ObjectKeyword: return this.materializeStructuralClass(node, [], depth, substitutions); case ts.SyntaxKind.VoidKeyword: @@ -569,6 +572,7 @@ export class TypeConverter { if (flags & ts.TypeFlags.NumberLike) return NUMBER_TYPE; if (flags & ts.TypeFlags.BigIntLike) return BIGINT_TYPE; if (flags & ts.TypeFlags.StringLike) return STRING_TYPE; + if (flags & ts.TypeFlags.ESSymbolLike) return SYMBOL_TYPE; if (flags & ts.TypeFlags.TypeParameter) { return { _: "GenericType", name: this.typeName(type) }; diff --git a/jacodb-ets/ts-frontend/test/types.spec.ts b/jacodb-ets/ts-frontend/test/types.spec.ts index d56eb6b32..7e72bb9ff 100644 --- a/jacodb-ets/ts-frontend/test/types.spec.ts +++ b/jacodb-ets/ts-frontend/test/types.spec.ts @@ -39,6 +39,7 @@ describe("convertTypeNode (annotations)", () => { expect(annotationOf("let x: undefined;")).toEqual({ _: "UndefinedType" }); expect(annotationOf("let x: never;")).toEqual({ _: "NeverType" }); expect(annotationOf("let x: bigint;")).toEqual({ _: "BigIntType" }); + expect(annotationOf("let x: symbol;")).toEqual({ _: "SymbolType" }); }); it("converts literal types with bare JSON primitives", () => { @@ -342,6 +343,7 @@ describe("typeOfNode (inference)", () => { expect(inferredOf(`let x = "s";`)).toEqual({ _: "StringType" }); expect(inferredOf("let x = true;")).toEqual({ _: "BooleanType" }); expect(inferredOf("let x = 1n;")).toEqual({ _: "BigIntType" }); + expect(inferredOf("let x = Symbol();")).toEqual({ _: "SymbolType" }); }); it("keeps literal types for const-declarations", () => {