From aa319129f8b634d3bc1b6b85376a22f06e8739b9 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sat, 29 Aug 2026 21:13:54 +0300 Subject: [PATCH] [ETS] Preserve type-only export metadata --- .../main/kotlin/org/jacodb/ets/dto/Convert.kt | 1 + .../main/kotlin/org/jacodb/ets/dto/Model.kt | 1 + .../kotlin/org/jacodb/ets/model/Export.kt | 2 ++ .../org/jacodb/ets/test/EtsExportTest.kt | 30 +++++++++++++++++++ jacodb-ets/ts-frontend/src/dto/model.ts | 1 + .../ts-frontend/src/lowering/fileBuilder.ts | 18 +++++++++-- jacodb-ets/ts-frontend/test/imports.spec.ts | 29 ++++++++++++++---- 7 files changed, 75 insertions(+), 7 deletions(-) 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 16f6e4ade..3a7457182 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 @@ -852,6 +852,7 @@ fun ExportInfoDto.toEtsExportInfo(): EtsExportInfo { from = exportFrom, nameBeforeAs = nameBeforeAs, modifiers = EtsModifiers(modifiers), + isTypeOnly = isTypeOnly, ) } diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt index 860af8b11..7537cb513 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Model.kt @@ -135,6 +135,7 @@ data class ExportInfoDto( val exportFrom: String? = null, val nameBeforeAs: String? = null, val modifiers: Int, + val isTypeOnly: Boolean = false, // val decorators: List, ) diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Export.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Export.kt index 7158beeb0..4f2d0460c 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Export.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Export.kt @@ -24,6 +24,7 @@ package org.jacodb.ets.model * @property from The module or path being exported from (null for direct exports). * @property nameBeforeAs The original name before 'as' aliasing (null if no aliasing). * @property modifiers Export modifiers. + * @property isTypeOnly Whether this export is only available in type positions. */ data class EtsExportInfo( val name: String, @@ -31,6 +32,7 @@ data class EtsExportInfo( val from: String? = null, val nameBeforeAs: String? = null, override val modifiers: EtsModifiers = EtsModifiers.EMPTY, + val isTypeOnly: Boolean = false, ) : Base { // Note: Export statements do not have decorators in JS/TS. diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsExportTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsExportTest.kt index 10d6994cb..4eb5e3c85 100644 --- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsExportTest.kt +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsExportTest.kt @@ -16,13 +16,17 @@ package org.jacodb.ets.test +import kotlinx.serialization.json.Json import mu.KotlinLogging +import org.jacodb.ets.dto.ExportInfoDto +import org.jacodb.ets.dto.toEtsExportInfo import org.jacodb.ets.model.EtsFile import org.jacodb.ets.test.utils.getResourcePath import org.jacodb.ets.utils.loadEtsFileAutoConvert import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -212,6 +216,32 @@ class EtsExportTest { logger.info { "✓ Namespace export test passed: $namespaceExport" } } + @Test + fun testExportInfoTypeOnlyMetadata() { + val typeOnlyExport = ExportInfoDto( + exportName = "*", + exportType = 9, + exportFrom = "./types", + modifiers = 0, + isTypeOnly = true, + ).toEtsExportInfo() + + assertTrue(typeOnlyExport.isTypeOnly) + + val legacyExport = Json.decodeFromString( + """ + { + "exportName": "runtimeValue", + "exportType": 3, + "exportFrom": "./values", + "modifiers": 0 + } + """.trimIndent() + ).toEtsExportInfo() + + assertFalse(legacyExport.isTypeOnly) + } + @Test fun testExportInfoToString() { logger.info { "Testing EtsExportInfo toString() method" } diff --git a/jacodb-ets/ts-frontend/src/dto/model.ts b/jacodb-ets/ts-frontend/src/dto/model.ts index 12dab62dc..1407fabf5 100644 --- a/jacodb-ets/ts-frontend/src/dto/model.ts +++ b/jacodb-ets/ts-frontend/src/dto/model.ts @@ -134,6 +134,7 @@ export interface ExportInfoDto { exportFrom?: string; // Kotlin default: null nameBeforeAs?: string; // Kotlin default: null modifiers: number; + isTypeOnly: boolean; } export interface DecoratorDto { diff --git a/jacodb-ets/ts-frontend/src/lowering/fileBuilder.ts b/jacodb-ets/ts-frontend/src/lowering/fileBuilder.ts index 4d9ddf4e2..34bd5ee6c 100644 --- a/jacodb-ets/ts-frontend/src/lowering/fileBuilder.ts +++ b/jacodb-ets/ts-frontend/src/lowering/fileBuilder.ts @@ -223,11 +223,17 @@ class FileBuilder { exportName: name, exportType: exportTypeOfDeclaration(statement), modifiers, + isTypeOnly: false, }); } else if (ts.isVariableStatement(statement)) { for (const decl of statement.declarationList.declarations) { if (ts.isIdentifier(decl.name)) { - infos.push({ exportName: decl.name.text, exportType: ExportType.LOCAL, modifiers }); + infos.push({ + exportName: decl.name.text, + exportType: ExportType.LOCAL, + modifiers, + isTypeOnly: false, + }); } } } @@ -241,7 +247,12 @@ class FileBuilder { : undefined; if (statement.exportClause === undefined) { // `export * from "module"`. - const info: ExportInfoDto = { exportName: "*", exportType: ExportType.UNKNOWN, modifiers: 0 }; + const info: ExportInfoDto = { + exportName: "*", + exportType: ExportType.UNKNOWN, + modifiers: 0, + isTypeOnly: statement.isTypeOnly, + }; if (exportFrom !== undefined) info.exportFrom = exportFrom; infos.push(info); } else if (ts.isNamedExports(statement.exportClause)) { @@ -250,6 +261,7 @@ class FileBuilder { exportName: element.name.text, exportType: this.exportTypeOfSymbol(element.name), modifiers: 0, + isTypeOnly: statement.isTypeOnly || element.isTypeOnly, }; if (element.propertyName !== undefined) info.nameBeforeAs = element.propertyName.text; if (exportFrom !== undefined) info.exportFrom = exportFrom; @@ -263,6 +275,7 @@ class FileBuilder { exportType: ExportType.NAMESPACE, nameBeforeAs: "*", modifiers: 0, + isTypeOnly: statement.isTypeOnly, }; if (exportFrom !== undefined) info.exportFrom = exportFrom; infos.push(info); @@ -276,6 +289,7 @@ class FileBuilder { exportName: name, exportType: this.exportTypeOfSymbol(statement.expression), modifiers: Modifier.DEFAULT, + isTypeOnly: false, }); } } diff --git a/jacodb-ets/ts-frontend/test/imports.spec.ts b/jacodb-ets/ts-frontend/test/imports.spec.ts index 3522b6fa1..b28fc4775 100644 --- a/jacodb-ets/ts-frontend/test/imports.spec.ts +++ b/jacodb-ets/ts-frontend/test/imports.spec.ts @@ -57,10 +57,27 @@ describe("export infos", () => { export * as bundle from "./bundle"; `); expect(file.exportInfos).toEqual([ - { exportName: "X", exportType: 9, modifiers: 0, exportFrom: "./other" }, - { exportName: "Z", exportType: 9, nameBeforeAs: "Y", modifiers: 0, exportFrom: "./other" }, - { exportName: "*", exportType: 9, modifiers: 0, exportFrom: "./star" }, - { exportName: "bundle", exportType: 0, nameBeforeAs: "*", modifiers: 0, exportFrom: "./bundle" }, + { exportName: "X", exportType: 9, modifiers: 0, exportFrom: "./other", isTypeOnly: false }, + { exportName: "Z", exportType: 9, nameBeforeAs: "Y", modifiers: 0, exportFrom: "./other", isTypeOnly: false }, + { exportName: "*", exportType: 9, modifiers: 0, exportFrom: "./star", isTypeOnly: false }, + { exportName: "bundle", exportType: 0, nameBeforeAs: "*", modifiers: 0, exportFrom: "./bundle", isTypeOnly: false }, + ]); + }); + + it("preserves type-only metadata for re-exports", () => { + const { file } = lower(` + export * from "./values"; + export type * from "./types"; + export { runtimeValue, type RuntimeType } from "./mixed"; + export type { DeclaredType } from "./declared"; + `); + + expect(file.exportInfos).toEqual([ + { exportName: "*", exportType: 9, modifiers: 0, exportFrom: "./values", isTypeOnly: false }, + { exportName: "*", exportType: 9, modifiers: 0, exportFrom: "./types", isTypeOnly: true }, + { exportName: "runtimeValue", exportType: 9, modifiers: 0, exportFrom: "./mixed", isTypeOnly: false }, + { exportName: "RuntimeType", exportType: 9, modifiers: 0, exportFrom: "./mixed", isTypeOnly: true }, + { exportName: "DeclaredType", exportType: 9, modifiers: 0, exportFrom: "./declared", isTypeOnly: true }, ]); }); @@ -81,7 +98,7 @@ describe("export infos", () => { export default Main; `); expect(file.exportInfos).toEqual([ - { exportName: "Main", exportType: 1, modifiers: Modifier.DEFAULT }, + { exportName: "Main", exportType: 1, modifiers: Modifier.DEFAULT, isTypeOnly: false }, ]); }); @@ -103,6 +120,7 @@ describe("export infos", () => { exportName: "default", exportType: 2, modifiers: Modifier.EXPORT | Modifier.DEFAULT, + isTypeOnly: false, }); const anonymousClass = lower(`export default class {}`).file; @@ -113,6 +131,7 @@ describe("export infos", () => { exportName: "default", exportType: 1, modifiers: Modifier.EXPORT | Modifier.DEFAULT, + isTypeOnly: false, }); }); });