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