@@ -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