@@ -295,6 +295,8 @@ public final class SwiftToSkeleton {
295295 collector . finalize ( & exported )
296296 }
297297
298+ perSourceErrors . append ( contentsOf : diagnoseProtocolConformances ( in : exported ) )
299+
298300 if ! perSourceErrors . isEmpty {
299301 let diagnostics = perSourceErrors . flatMap { inputFilePath , errors in
300302 errors . map { ( file : inputFilePath , diagnostic : $0 ) }
@@ -318,6 +320,99 @@ public final class SwiftToSkeleton {
318320 )
319321 }
320322
323+ private func diagnoseProtocolConformances (
324+ in exported : ExportedSkeleton
325+ ) -> [ ( inputFilePath : String , errors : [ DiagnosticError ] ) ] {
326+ var loweredProtocols : Set < String > = [ ]
327+ func collect ( _ type : BridgeType , loweredBySwift : Bool ) {
328+ switch type {
329+ case . swiftProtocol ( let name ) :
330+ if loweredBySwift { loweredProtocols . insert ( name ) }
331+ case . array ( let element ) , . dictionary ( let element ) , . nullable ( let element , _) , . alias ( _, let element ) :
332+ collect ( element , loweredBySwift : loweredBySwift )
333+ case . closure ( let signature , _) :
334+ for parameter in signature . parameters {
335+ collect ( parameter , loweredBySwift : ! loweredBySwift )
336+ }
337+ collect ( signature . returnType , loweredBySwift : loweredBySwift )
338+ default :
339+ break
340+ }
341+ }
342+ func collect ( _ function : ExportedFunction , loweredReturn : Bool = true ) {
343+ for parameter in function . parameters { collect ( parameter . type , loweredBySwift : ! loweredReturn ) }
344+ collect ( function . returnType , loweredBySwift : loweredReturn )
345+ }
346+
347+ for function in exported . functions + exported . classes . flatMap ( \. methods )
348+ + exported . structs . flatMap ( \. methods ) + exported . enums . flatMap ( \. staticMethods )
349+ {
350+ collect ( function )
351+ }
352+ for constructor in exported . classes . compactMap ( \. constructor ) + exported . structs . compactMap ( \. constructor ) {
353+ for parameter in constructor . parameters { collect ( parameter . type , loweredBySwift : false ) }
354+ }
355+ for property in exported . classes . flatMap ( \. properties ) + exported . enums . flatMap ( \. staticProperties ) {
356+ collect ( property . type , loweredBySwift : true )
357+ if ! property . isReadonly { collect ( property . type , loweredBySwift : false ) }
358+ }
359+ for property in exported . structs . flatMap ( \. properties ) {
360+ collect ( property . type , loweredBySwift : true )
361+ if ! property . isStatic || ! property . isReadonly { collect ( property . type , loweredBySwift : false ) }
362+ }
363+ for value in exported . enums . flatMap ( \. cases ) . flatMap ( \. associatedValues ) {
364+ collect ( value . type , loweredBySwift : true )
365+ }
366+ for protocolDef in exported . protocols {
367+ for method in protocolDef . methods { collect ( method , loweredReturn : false ) }
368+ for property in protocolDef . properties {
369+ collect ( property . type , loweredBySwift : false )
370+ if ! property . isReadonly { collect ( property . type , loweredBySwift : true ) }
371+ }
372+ }
373+
374+ guard ! loweredProtocols . isEmpty else { return [ ] }
375+ var diagnostics : [ ( inputFilePath : String , errors : [ DiagnosticError ] ) ] = [ ]
376+ for declaration in typeDeclResolver . declarationsWithInheritance where ! declaration . is ( ProtocolDeclSyntax . self ) {
377+ let extendedType = declaration . as ( ExtensionDeclSyntax . self ) ? . extendedType
378+ let target = extendedType . flatMap { typeDeclResolver . resolveExtensionTarget ( $0 ) }
379+ let classDecl = declaration . as ( ClassDeclSyntax . self ) ?? target ? . as ( ClassDeclSyntax . self )
380+ if classDecl ? . attributes . hasJSAttribute ( ) == true { continue }
381+ if let extendedType , target == nil {
382+ var errors : [ DiagnosticError ] = [ ]
383+ if case . swiftHeapObject = resolveExternal ( for : extendedType , errors : & errors ) { continue }
384+ }
385+ guard
386+ let name = declaration . asProtocol ( NamedDeclSyntax . self ) ? . name . text ?? extendedType ? . trimmedDescription ,
387+ let inputFilePath = sourceFiles . first ( where : { $0 . sourceFile . id == declaration . root . id } ) ? . inputFilePath
388+ else { continue }
389+ for inherited in declaration . inheritanceClause ? . inheritedTypes ?? [ ] {
390+ guard let protocolDecl = typeDeclResolver . resolve ( inherited . type ) ? . as ( ProtocolDeclSyntax . self ) ,
391+ protocolDecl . attributes . hasJSAttribute ( ) , loweredProtocols . contains ( protocolDecl . name . text )
392+ else { continue }
393+ let protocolName = protocolDecl . name . text
394+ diagnostics . append (
395+ (
396+ inputFilePath ,
397+ [
398+ DiagnosticError (
399+ node : declaration ,
400+ message :
401+ " ' \( name ) ' conforms to ' \( protocolName ) ', a @JS protocol that exported APIs "
402+ + " bridge to JavaScript, but ' \( name ) ' is not a '@JS class'. Passing it to "
403+ + " JavaScript as 'any \( protocolName ) ' would trap at runtime. " ,
404+ hint :
405+ " Mark ' \( name ) ' as a '@JS class' so it can cross the bridge, or avoid using "
406+ + " ' \( protocolName ) ' as an existential in exported APIs. "
407+ )
408+ ]
409+ )
410+ )
411+ }
412+ }
413+ return diagnostics
414+ }
415+
321416 private static let jsTypedArrayTypealiasNames : [ String : String ] = [
322417 " Int8 " : " JSInt8Array " ,
323418 " UInt8 " : " JSUint8Array " ,
0 commit comments