@@ -24,6 +24,9 @@ public final class SwiftToSkeleton {
2424 private var sourceFiles : [ ( sourceFile: SourceFileSyntax , inputFilePath: String ) ] = [ ]
2525 private var usedExternalModules = Set < String > ( )
2626
27+ /// Non-fatal diagnostics collected during `finalize()`. These do not fail the build.
28+ public private( set) var warnings : [ ( file: String , diagnostic: DiagnosticError ) ] = [ ]
29+
2730 public init (
2831 progress: ProgressReporting ,
2932 moduleName: String ,
@@ -87,10 +90,15 @@ public final class SwiftToSkeleton {
8790 )
8891 importCollector. walk ( sourceFile)
8992
90- let importErrorsFatal = importCollector. errors. filter { !$0. message. contains ( " Unsupported type ' " ) }
91- if !exportCollector. errors. isEmpty || !importErrorsFatal. isEmpty {
93+ let exportErrors = exportCollector. errors. filter { $0. severity == . error }
94+ let importErrorsFatal = importCollector. errors. filter {
95+ $0. severity == . error && !$0. message. contains ( " Unsupported type ' " )
96+ }
97+ let fileWarnings = ( exportCollector. errors + importCollector. errors) . filter { $0. severity == . warning }
98+ warnings. append ( contentsOf: fileWarnings. map { ( file: inputFilePath, diagnostic: $0) } )
99+ if !exportErrors. isEmpty || !importErrorsFatal. isEmpty {
92100 perSourceErrors. append (
93- ( inputFilePath: inputFilePath, errors: exportCollector . errors + importErrorsFatal)
101+ ( inputFilePath: inputFilePath, errors: exportErrors + importErrorsFatal)
94102 )
95103 }
96104
@@ -191,7 +199,40 @@ public final class SwiftToSkeleton {
191199 }
192200
193201 let isAsync = functionType. effectSpecifiers? . asyncSpecifier != nil
194- let isThrows = functionType. effectSpecifiers? . throwsClause != nil
202+
203+ if isAsync, !returnType. isAsyncResolvable {
204+ errors. append (
205+ DiagnosticError (
206+ node: functionType,
207+ message:
208+ " Returning ' \( returnType. swiftType) ' from an async closure is not yet supported " ,
209+ hint:
210+ " Return a type lowerable through the async resolve ABI "
211+ + " (String/Int/Bool/Double/Float/raw-value or case-only enum/@JS struct/JSObject/Optional/Array/Dictionary), "
212+ + " or make the closure non-async. "
213+ )
214+ )
215+ return nil
216+ }
217+
218+ var isThrows = false
219+ if let throwsClause = functionType. effectSpecifiers? . throwsClause {
220+ guard let thrownType = throwsClause. type,
221+ thrownType. trimmedDescription == " JSException "
222+ else {
223+ errors. append (
224+ DiagnosticError (
225+ node: throwsClause,
226+ message:
227+ " Only JSException is supported for thrown type of Swift closures, "
228+ + " got \( throwsClause. type? . trimmedDescription ?? " unspecified " ) " ,
229+ hint: " Annotate the closure as `throws(JSException)` "
230+ )
231+ )
232+ return nil
233+ }
234+ isThrows = true
235+ }
195236
196237 return . closure(
197238 ClosureSignature (
@@ -569,6 +610,37 @@ private enum ExportSwiftConstants {
569610 static let supportedRawTypes = SwiftEnumRawType . supportedTypeNames
570611}
571612
613+ /// Warns about Swift closures handed to JavaScript with an `async throws(JSException)` signature.
614+ /// Captureless closure values lose their thrown error at runtime due to a Swift compiler bug.
615+ private func asyncThrowsClosureWarning( node: some SyntaxProtocol ) -> DiagnosticError {
616+ DiagnosticError (
617+ node: node,
618+ message:
619+ " async throwing closures passed to JavaScript may lose thrown errors due to a Swift compiler bug "
620+ + " (swiftlang/swift#89320) unless the closure value captures state " ,
621+ hint:
622+ " Pass a closure that captures state, or see the BridgeJS closure documentation for details " ,
623+ severity: . warning
624+ )
625+ }
626+
627+ extension BridgeType {
628+ fileprivate var containsAsyncThrowsClosure : Bool {
629+ switch self {
630+ case . closure( let signature, _) :
631+ return signature. isAsync && signature. isThrows
632+ case . nullable( let wrapped, _) :
633+ return wrapped. containsAsyncThrowsClosure
634+ case . array( let element) :
635+ return element. containsAsyncThrowsClosure
636+ case . dictionary( let value) :
637+ return value. containsAsyncThrowsClosure
638+ default :
639+ return false
640+ }
641+ }
642+ }
643+
572644extension AttributeSyntax {
573645 /// The attribute name as text when it is a simple identifier (e.g. "JS", "JSFunction").
574646 /// Prefer this over `attributeName.trimmedDescription` for name checks to avoid unnecessary string work.
@@ -1028,22 +1100,6 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
10281100 guard let type = resolvedType else {
10291101 continue // Skip unsupported types
10301102 }
1031- if case . closure( let signature, _) = type {
1032- if signature. isAsync {
1033- diagnose (
1034- node: param. type,
1035- message: " Async is not supported for Swift closures yet. "
1036- )
1037- continue
1038- }
1039- if signature. isThrows {
1040- diagnose (
1041- node: param. type,
1042- message: " Throws is not supported for Swift closures yet. "
1043- )
1044- continue
1045- }
1046- }
10471103 if case . nullable( let wrappedType, _) = type, wrappedType. isOptional {
10481104 diagnoseNestedOptional ( node: param. type, type: param. type. trimmedDescription)
10491105 continue
@@ -1177,6 +1233,9 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
11771233
11781234 guard let type = resolvedType else { return nil }
11791235 returnType = type
1236+ if returnType. containsAsyncThrowsClosure {
1237+ errors. append ( asyncThrowsClosureWarning ( node: returnClause. type) )
1238+ }
11801239 } else {
11811240 returnType = . void
11821241 }
@@ -2836,6 +2895,11 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
28362895 guard let bridgeType = withLookupErrors ( { parent. lookupType ( for: type, errors: & $0) } ) else {
28372896 return nil
28382897 }
2898+ if case . closure( let signature, useJSTypedClosure: true ) = bridgeType,
2899+ signature. isAsync, signature. isThrows
2900+ {
2901+ errors. append ( asyncThrowsClosureWarning ( node: type) )
2902+ }
28392903 let nameToken = param. secondName ?? param. firstName
28402904 let name = SwiftToSkeleton . normalizeIdentifier ( nameToken. text)
28412905 let labelToken = param. secondName == nil ? nil : param. firstName
0 commit comments