From 25683e49437935e84051ea885926f3431a041f82 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Feb 2026 13:05:19 +0900 Subject: [PATCH] =?UTF-8?q?-=20Added=20Record=20detection=20in=20`Plugins/?= =?UTF-8?q?BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js`,=20mappi?= =?UTF-8?q?ng=20`Record`=20to=20`[String:=20T]`=20(with=20JSO?= =?UTF-8?q?bject=20fallback=20for=20non-string=20keys)=20by=20reading=20al?= =?UTF-8?q?ias=20type=20arguments=20even=20when=20the=20type=20isn?= =?UTF-8?q?=E2=80=99t=20a=20`TypeReference`.=20-=20Added=20a=20new=20fixtu?= =?UTF-8?q?re=20and=20refreshed=20Vitest=20snapshots=20to=20cover=20dictio?= =?UTF-8?q?nary=20outputs,=20nested/optional=20records,=20array/object=20v?= =?UTF-8?q?alues,=20and=20unsupported=20key=20handling=20in=20`Plugins/Bri?= =?UTF-8?q?dgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionar?= =?UTF-8?q?y.d.ts`=20and=20`.../=5F=5Fsnapshots=5F=5F/ts2swift.test.js.sna?= =?UTF-8?q?p`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests: `cd Plugins/BridgeJS/Sources/TS2Swift/JavaScript && npm test -- --update`. --- .../TS2Swift/JavaScript/src/processor.js | 54 +++++++++++++++++++ .../test/__snapshots__/ts2swift.test.js.snap | 30 +++++++++++ .../test/fixtures/RecordDictionary.d.ts | 17 ++++++ 3 files changed, 101 insertions(+) create mode 100644 Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index ef446af0..04578e9e 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -663,6 +663,7 @@ export class TypeProcessor { * @private */ visitType(type, node) { + const typeArguments = this.getTypeArguments(type); if (this.checker.isArrayType(type)) { const typeArgs = this.checker.getTypeArguments(type); if (typeArgs && typeArgs.length > 0) { @@ -672,6 +673,11 @@ export class TypeProcessor { return "[JSObject]"; } + const recordType = this.convertRecordType(type, typeArguments, node); + if (recordType) { + return recordType; + } + // Treat A and A as the same type if (isTypeReference(type)) { type = type.target; @@ -760,6 +766,54 @@ export class TypeProcessor { return swiftType; } + /** + * Convert a `Record` TypeScript type into a Swift dictionary type. + * Falls back to `JSObject` when keys are not string-compatible or type arguments are missing. + * @param {ts.Type} type + * @param {ts.Type[]} typeArguments + * @param {ts.Node} node + * @returns {string | null} + * @private + */ + convertRecordType(type, typeArguments, node) { + const symbol = type.aliasSymbol ?? type.getSymbol(); + if (!symbol || symbol.name !== "Record") { + return null; + } + if (typeArguments.length !== 2) { + this.diagnosticEngine.print("warning", "Record expects two type arguments", node); + return "JSObject"; + } + const [keyType, valueType] = typeArguments; + const stringType = this.checker.getStringType(); + if (!this.checker.isTypeAssignableTo(keyType, stringType)) { + this.diagnosticEngine.print( + "warning", + `Record key type must be assignable to string: ${this.checker.typeToString(keyType)}`, + node + ); + return "JSObject"; + } + + const valueSwiftType = this.visitType(valueType, node); + return `[String: ${valueSwiftType}]`; + } + + /** + * Retrieve type arguments for a given type, including type alias instantiations. + * @param {ts.Type} type + * @returns {ts.Type[]} + * @private + */ + getTypeArguments(type) { + if (isTypeReference(type)) { + return this.checker.getTypeArguments(type); + } + // Non-TypeReference alias instantiations store type arguments separately. + // @ts-ignore: `aliasTypeArguments` is intentionally accessed for alias instantiations. + return type.aliasTypeArguments ?? []; + } + /** * Derive the type name from a type * @param {ts.Type} type - TypeScript type diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 6fdb79f7..e6fb234c 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -251,6 +251,36 @@ exports[`ts2swift > snapshots Swift output for ReExportFrom.d.ts > ReExportFrom " `; +exports[`ts2swift > snapshots Swift output for RecordDictionary.d.ts > RecordDictionary 1`] = ` +"// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// \`swift package bridge-js\`. + +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit + +@JSFunction func takeRecord(_ value: [String: Double]) throws(JSException) -> Void + +@JSFunction func returnRecord() throws(JSException) -> [String: String] + +@JSFunction func optionalRecord(_ value: Optional<[String: Bool]>) throws(JSException) -> Optional<[String: Bool]> + +@JSFunction func nestedRecord(_ value: [String: [String: Double]]) throws(JSException) -> [String: [String: Double]] + +@JSFunction func recordWithArrayValues(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] + +@JSFunction func recordWithObjects(_ values: [String: Optional]) throws(JSException) -> [String: Optional] + +@JSClass struct Box { + @JSGetter var value: Double + @JSSetter func setValue(_ value: Double) throws(JSException) +} + +@JSFunction func unsupportedKeyRecord(_ values: JSObject) throws(JSException) -> Void +" +`; + exports[`ts2swift > snapshots Swift output for StringEnum.d.ts > StringEnum 1`] = ` "// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, // DO NOT EDIT. diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts new file mode 100644 index 00000000..cfaf5f30 --- /dev/null +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/RecordDictionary.d.ts @@ -0,0 +1,17 @@ +export interface Box { + value: number; +} + +export function takeRecord(value: Record): void; + +export function returnRecord(): Record; + +export function optionalRecord(value: Record | null): Record | null; + +export function nestedRecord(value: Record>): Record>; + +export function recordWithArrayValues(values: Record): Record; + +export function recordWithObjects(values: Record): Record; + +export function unsupportedKeyRecord(values: Record): void;