From 095d4e73f3c4828a675664d4c4e30d8f87b5e231 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 25 Aug 2025 14:18:23 +0900 Subject: [PATCH] BridgeJS: Skip importing TS declarations with invalid Swift identifiers --- .../Sources/BridgeJSCore/ImportTS.swift | 12 +- .../TS2Skeleton/JavaScript/src/processor.js | 30 ++- .../Inputs/InvalidPropertyNames.d.ts | 23 ++ .../InvalidPropertyNames.Import.d.ts | 29 +++ .../InvalidPropertyNames.Import.js | 168 ++++++++++++++ .../ImportTSTests/InvalidPropertyNames.swift | 205 ++++++++++++++++++ 6 files changed, 461 insertions(+), 6 deletions(-) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 148157d3..9fc8a62d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -156,7 +156,7 @@ public struct ImportTS { func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax { return DeclSyntax( FunctionDeclSyntax( - name: .identifier(name), + name: .identifier(name.backtickIfNeeded()), signature: FunctionSignatureSyntax( parameterClause: FunctionParameterClauseSyntax(parametersBuilder: { for param in parameters { @@ -315,7 +315,9 @@ public struct ImportTS { bindingsBuilder: { PatternBindingListSyntax { PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: .identifier(property.name)), + pattern: IdentifierPatternSyntax( + identifier: .identifier(property.name.backtickIfNeeded()) + ), typeAnnotation: TypeAnnotationSyntax( type: IdentifierTypeSyntax(name: .identifier(property.type.swiftType)) ), @@ -466,3 +468,9 @@ extension BridgeType { } } } + +extension String { + func backtickIfNeeded() -> String { + return self.isValidSwiftIdentifier(for: .variableName) ? self : "`\(self)`" + } +} diff --git a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/processor.js index aeaf6a2d..cdcff719 100644 --- a/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/processor.js @@ -142,6 +142,10 @@ export class TypeProcessor { */ visitFunctionLikeDecl(node) { if (!node.name) return null; + const name = node.name.getText(); + if (!isValidSwiftDeclName(name)) { + return null; + } const signature = this.checker.getSignatureFromDeclaration(node); if (!signature) return null; @@ -158,7 +162,7 @@ export class TypeProcessor { const documentation = this.getFullJSDocText(node); return { - name: node.name.getText(), + name, parameters, returnType: bridgeReturnType, documentation, @@ -206,11 +210,17 @@ export class TypeProcessor { */ visitPropertyDecl(node) { if (!node.name) return null; + + const propertyName = node.name.getText(); + if (!isValidSwiftDeclName(propertyName)) { + return null; + } + const type = this.checker.getTypeAtLocation(node) const bridgeType = this.visitType(type, node); const isReadonly = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false; const documentation = this.getFullJSDocText(node); - return { name: node.name.getText(), type: bridgeType, isReadonly, documentation }; + return { name: propertyName, type: bridgeType, isReadonly, documentation }; } /** @@ -225,7 +235,7 @@ export class TypeProcessor { } /** - * @param {ts.ClassDeclaration} node + * @param {ts.ClassDeclaration} node * @returns {ImportTypeSkeleton | null} */ visitClassDecl(node) { @@ -442,4 +452,16 @@ function isTypeReference(type) { isObjectType(type) && (type.objectFlags & ts.ObjectFlags.Reference) !== 0 ); -} \ No newline at end of file +} + +/** + * Check if a declaration name is valid for Swift generation + * @param {string} name - Declaration name to check + * @returns {boolean} True if the name is valid for Swift + * @private + */ +export function isValidSwiftDeclName(name) { + // https://docs.swift.org/swift-book/documentation/the-swift-programming-language/lexicalstructure/ + const swiftIdentifierRegex = /^[_\p{ID_Start}][\p{ID_Continue}\u{200C}\u{200D}]*$/u; + return swiftIdentifierRegex.test(name); +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts new file mode 100644 index 00000000..d21f3c20 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/InvalidPropertyNames.d.ts @@ -0,0 +1,23 @@ +interface ArrayBufferLike { + readonly byteLength: number; + readonly [Symbol.toStringTag]: string; + slice(begin: number, end: number): ArrayBufferLike; +} + +interface WeirdNaming { + normalProperty: string; + "property-with-dashes": number; + "123invalidStart": boolean; + "property with spaces": string; + readonly [Symbol.species]: any; + [Symbol.asyncIterator](): AsyncIterator; + "@specialChar": number; + "constructor": string; // This should be valid + for: string; + Any: string; + as(): void; + "try"(): void; +} + +export function createArrayBuffer(): ArrayBufferLike; +export function createWeirdObject(): WeirdNaming; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts new file mode 100644 index 00000000..2b0474bb --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.d.ts @@ -0,0 +1,29 @@ +// 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`. + +export interface ArrayBufferLike { + slice(begin: number, end: number): ArrayBufferLike; + readonly byteLength: number; +} +export interface WeirdNaming { + as(): void; + normalProperty: string; + for: string; + Any: string; +} +export type Exports = { +} +export type Imports = { + createArrayBuffer(): ArrayBufferLike; + createWeirdObject(): WeirdNaming; +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js new file mode 100644 index 00000000..6b5211e0 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/InvalidPropertyNames.Import.js @@ -0,0 +1,168 @@ +// 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`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + const bjs = {}; + importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); + bjs["swift_js_return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_createArrayBuffer"] = function bjs_createArrayBuffer() { + try { + let ret = imports.createArrayBuffer(); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_createWeirdObject"] = function bjs_createWeirdObject() { + try { + let ret = imports.createWeirdObject(); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_ArrayBufferLike_byteLength_get"] = function bjs_ArrayBufferLike_byteLength_get(self) { + try { + let ret = swift.memory.getObject(self).byteLength; + return ret; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_ArrayBufferLike_slice"] = function bjs_ArrayBufferLike_slice(self, begin, end) { + try { + let ret = swift.memory.getObject(self).slice(begin, end); + return swift.memory.retain(ret); + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_WeirdNaming_normalProperty_get"] = function bjs_WeirdNaming_normalProperty_get(self) { + try { + let ret = swift.memory.getObject(self).normalProperty; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_normalProperty_set"] = function bjs_WeirdNaming_normalProperty_set(self, newValue) { + try { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).normalProperty = newValueObject; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_for_get"] = function bjs_WeirdNaming_for_get(self) { + try { + let ret = swift.memory.getObject(self).for; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_for_set"] = function bjs_WeirdNaming_for_set(self, newValue) { + try { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).for = newValueObject; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_Any_get"] = function bjs_WeirdNaming_Any_get(self) { + try { + let ret = swift.memory.getObject(self).Any; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_Any_set"] = function bjs_WeirdNaming_Any_set(self, newValue) { + try { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).Any = newValueObject; + } catch (error) { + setException(error); + } + } + TestModule["bjs_WeirdNaming_as"] = function bjs_WeirdNaming_as(self) { + try { + swift.memory.getObject(self).as(); + } catch (error) { + setException(error); + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + + return { + + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift new file mode 100644 index 00000000..7e35f921 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/InvalidPropertyNames.swift @@ -0,0 +1,205 @@ +// 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(BridgeJS) import JavaScriptKit + +func createArrayBuffer() throws(JSException) -> ArrayBufferLike { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_createArrayBuffer") + func bjs_createArrayBuffer() -> Int32 + #else + func bjs_createArrayBuffer() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_createArrayBuffer() + if let error = _swift_js_take_exception() { + throw error + } + return ArrayBufferLike.bridgeJSLiftReturn(ret) +} + +func createWeirdObject() throws(JSException) -> WeirdNaming { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_createWeirdObject") + func bjs_createWeirdObject() -> Int32 + #else + func bjs_createWeirdObject() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_createWeirdObject() + if let error = _swift_js_take_exception() { + throw error + } + return WeirdNaming.bridgeJSLiftReturn(ret) +} + +struct ArrayBufferLike: _JSBridgedClass { + let jsObject: JSObject + + init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + + var byteLength: Double { + get throws(JSException) { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_ArrayBufferLike_byteLength_get") + func bjs_ArrayBufferLike_byteLength_get(_ self: Int32) -> Float64 + #else + func bjs_ArrayBufferLike_byteLength_get(_ self: Int32) -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_ArrayBufferLike_byteLength_get(self.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + return Double.bridgeJSLiftReturn(ret) + } + } + + func slice(_ begin: Double, _ end: Double) throws(JSException) -> ArrayBufferLike { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_ArrayBufferLike_slice") + func bjs_ArrayBufferLike_slice(_ self: Int32, _ begin: Float64, _ end: Float64) -> Int32 + #else + func bjs_ArrayBufferLike_slice(_ self: Int32, _ begin: Float64, _ end: Float64) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_ArrayBufferLike_slice(self.bridgeJSLowerParameter(), begin.bridgeJSLowerParameter(), end.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + return ArrayBufferLike.bridgeJSLiftReturn(ret) + } + +} + +struct WeirdNaming: _JSBridgedClass { + let jsObject: JSObject + + init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + + var normalProperty: String { + get throws(JSException) { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_normalProperty_get") + func bjs_WeirdNaming_normalProperty_get(_ self: Int32) -> Int32 + #else + func bjs_WeirdNaming_normalProperty_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_WeirdNaming_normalProperty_get(self.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) + } + } + + func setNormalProperty(_ newValue: String) throws(JSException) -> Void { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_normalProperty_set") + func bjs_WeirdNaming_normalProperty_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_WeirdNaming_normalProperty_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + bjs_WeirdNaming_normalProperty_set(self.bridgeJSLowerParameter(), newValue.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + } + + var `for`: String { + get throws(JSException) { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_for_get") + func bjs_WeirdNaming_for_get(_ self: Int32) -> Int32 + #else + func bjs_WeirdNaming_for_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_WeirdNaming_for_get(self.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) + } + } + + func setFor(_ newValue: String) throws(JSException) -> Void { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_for_set") + func bjs_WeirdNaming_for_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_WeirdNaming_for_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + bjs_WeirdNaming_for_set(self.bridgeJSLowerParameter(), newValue.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + } + + var `Any`: String { + get throws(JSException) { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_Any_get") + func bjs_WeirdNaming_Any_get(_ self: Int32) -> Int32 + #else + func bjs_WeirdNaming_Any_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_WeirdNaming_Any_get(self.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) + } + } + + func setAny(_ newValue: String) throws(JSException) -> Void { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_Any_set") + func bjs_WeirdNaming_Any_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_WeirdNaming_Any_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + bjs_WeirdNaming_Any_set(self.bridgeJSLowerParameter(), newValue.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + } + + func `as`() throws(JSException) -> Void { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_WeirdNaming_as") + func bjs_WeirdNaming_as(_ self: Int32) -> Void + #else + func bjs_WeirdNaming_as(_ self: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + bjs_WeirdNaming_as(self.bridgeJSLowerParameter()) + if let error = _swift_js_take_exception() { + throw error + } + } + +} \ No newline at end of file