@@ -7,6 +7,13 @@ import BridgeJSUtilities
77import BridgeJSSkeleton
88#endif
99
10+ /// Outcome of parsing the constraint clause of a generic parameter on a
11+ /// `@JS`/`@JSFunction` declaration.
12+ enum GenericConstraintParseResult {
13+ case parsed( GenericParameter )
14+ case failed( message: String )
15+ }
16+
1017/// Outcome of attempting to resolve a type as a reference to a generic parameter.
1118enum GenericParameterResolution {
1219 case resolved( BridgeType )
@@ -981,11 +988,85 @@ public final class SwiftToSkeleton {
981988 return name. unicodeScalars. dropFirst ( ) . allSatisfy { isIdentifierPart ( $0, isStart: false ) }
982989 }
983990
984- fileprivate static func isBridgeableGenericConstraint( _ constraint: String ? ) -> Bool {
991+ fileprivate static func isBridgeableGenericConstraint( _ constraint: String ) -> Bool {
985992 constraint == " BridgedSwiftGenericBridgeable "
986993 || constraint == " JavaScriptKit.BridgedSwiftGenericBridgeable "
987994 }
988995
996+ /// Returns the name of the `@JS protocol` referenced by `type`, or `nil` when the
997+ /// type does not resolve to a `@JS protocol` declared in this module or in a
998+ /// dependency module indexed by `ExternalModuleIndex`.
999+ fileprivate func resolveJSProtocolConstraint( for type: TypeSyntax ) -> String ? {
1000+ if let typeDecl = typeDeclResolver. resolve ( type) {
1001+ guard let protocolDecl = typeDecl. as ( ProtocolDeclSyntax . self) ,
1002+ protocolDecl. attributes. hasJSAttribute ( )
1003+ else {
1004+ return nil
1005+ }
1006+ return protocolDecl. name. text
1007+ }
1008+ var scratchErrors : [ DiagnosticError ] = [ ]
1009+ if let externalType = resolveExternal ( for: type, errors: & scratchErrors) ,
1010+ case . swiftProtocol( let name) = externalType
1011+ {
1012+ return name
1013+ }
1014+ return nil
1015+ }
1016+
1017+ /// Parses the constraint clause of a single generic parameter on a `@JS`/`@JSFunction`
1018+ /// declaration.
1019+ ///
1020+ /// The supported spellings are `BridgedSwiftGenericBridgeable` alone or a protocol
1021+ /// composition of `BridgedSwiftGenericBridgeable` with one or more `@JS` protocols
1022+ /// (in any order), e.g. `<T: BridgedSwiftGenericBridgeable & GraphNode>`.
1023+ fileprivate func parseGenericParameterConstraints(
1024+ _ genericParam: GenericParameterSyntax ,
1025+ attributeName: String
1026+ ) -> GenericConstraintParseResult {
1027+ let paramName = genericParam. name. text
1028+ guard let inheritedType = genericParam. inheritedType else {
1029+ return . failed(
1030+ message:
1031+ " Generic parameter ' \( paramName) ' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with \( attributeName) . "
1032+ )
1033+ }
1034+ let elements : [ TypeSyntax ]
1035+ if let composition = inheritedType. as ( CompositionTypeSyntax . self) {
1036+ elements = composition. elements. map { TypeSyntax ( $0. type) }
1037+ } else {
1038+ elements = [ inheritedType]
1039+ }
1040+ var hasBridgeableConstraint = false
1041+ var constraints : [ String ] = [ ]
1042+ for element in elements {
1043+ if SwiftToSkeleton . isBridgeableGenericConstraint ( element. trimmedDescription) {
1044+ hasBridgeableConstraint = true
1045+ continue
1046+ }
1047+ guard let protocolName = resolveJSProtocolConstraint ( for: element) else {
1048+ return . failed(
1049+ message:
1050+ " Generic parameter ' \( paramName) ' has unsupported constraint ' \( element. trimmedDescription) '. "
1051+ + " Constraints on generic \( attributeName) declarations must be 'BridgedSwiftGenericBridgeable', "
1052+ + " optionally composed with '@JS' protocols (e.g. ' \( paramName) : BridgedSwiftGenericBridgeable & MyProtocol'). "
1053+ )
1054+ }
1055+ if !constraints. contains ( protocolName) {
1056+ constraints. append ( protocolName)
1057+ }
1058+ }
1059+ guard hasBridgeableConstraint else {
1060+ let suggested = ( [ " BridgedSwiftGenericBridgeable " ] + constraints) . joined ( separator: " & " )
1061+ return . failed(
1062+ message:
1063+ " Generic parameter ' \( paramName) ' is missing the 'BridgedSwiftGenericBridgeable' constraint. "
1064+ + " Write ' \( paramName) : \( suggested) ' to combine it with '@JS' protocol constraints on \( attributeName) declarations. "
1065+ )
1066+ }
1067+ return . parsed( GenericParameter ( name: paramName, constraints: constraints) )
1068+ }
1069+
9891070}
9901071
9911072private enum ExportSwiftConstants {
@@ -1579,19 +1660,24 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
15791660 return nil
15801661 }
15811662
1582- var genericParameterNames : [ String ] = [ ]
1663+ var genericParameters : [ GenericParameter ] = [ ]
15831664 if let genericClause = node. genericParameterClause {
15841665 for genericParam in genericClause. parameters {
1585- let constraint = genericParam. inheritedType? . trimmedDescription
1586- guard SwiftToSkeleton . isBridgeableGenericConstraint ( constraint) else {
1587- diagnose (
1588- node: node,
1589- message:
1590- " Generic parameter ' \( genericParam. name. text) ' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JS. "
1591- )
1666+ switch parent. parseGenericParameterConstraints ( genericParam, attributeName: " @JS " ) {
1667+ case . failed( let message) :
1668+ diagnose ( node: node, message: message)
15921669 return nil
1670+ case . parsed( let genericParameter) :
1671+ if !genericParameter. constraints. isEmpty {
1672+ diagnose (
1673+ node: node,
1674+ message:
1675+ " '@JS' protocol constraints on generic parameters are not yet supported on exports. "
1676+ )
1677+ return nil
1678+ }
1679+ genericParameters. append ( genericParameter)
15931680 }
1594- genericParameterNames. append ( genericParam. name. text)
15951681 }
15961682 if node. genericWhereClause != nil {
15971683 diagnose (
@@ -1615,6 +1701,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16151701 return nil
16161702 }
16171703 }
1704+ let genericParameterNames = genericParameters. map ( \. name)
16181705
16191706 let name = node. name. text
16201707 let jsName = extractValidatedJSName ( from: jsAttribute)
@@ -1780,7 +1867,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17801867 namespace: finalNamespace,
17811868 staticContext: staticContext,
17821869 documentation: extractDocumentation ( from: node) ,
1783- genericParameters: genericParameterNames . isEmpty ? nil : genericParameterNames
1870+ genericParameters: genericParameters . isEmpty ? nil : genericParameters
17841871 )
17851872 }
17861873
@@ -3589,32 +3676,27 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
35893676
35903677 // MARK: - Parsing Methods
35913678
3592- /// Validates and collects the generic parameter names of an imported
3593- /// `@JSFunction` declaration (function, method or initializer).
3679+ /// Validates and collects the generic parameters of an imported
3680+ /// `@JSFunction` declaration (function, method or initializer), including
3681+ /// any `@JS protocol` constraints composed with `BridgedSwiftGenericBridgeable`.
35943682 ///
35953683 /// Returns `nil` when a diagnostic was emitted; an empty array when the
35963684 /// declaration is not generic.
3597- private func parseGenericParameterNames (
3685+ private func parseGenericParameters (
35983686 genericParameterClause: GenericParameterClauseSyntax ? ,
35993687 genericWhereClause: GenericWhereClauseSyntax ? ,
36003688 node: Syntax
3601- ) -> [ String ] ? {
3602- var genericParameterNames : [ String ] = [ ]
3689+ ) -> [ GenericParameter ] ? {
3690+ var genericParameters : [ GenericParameter ] = [ ]
36033691 if let genericParameterClause {
36043692 for genericParam in genericParameterClause. parameters {
3605- let paramName = genericParam. name. text
3606- let constraintText = genericParam. inheritedType? . trimmedDescription
3607- guard SwiftToSkeleton . isBridgeableGenericConstraint ( constraintText) else {
3608- errors. append (
3609- DiagnosticError (
3610- node: Syntax ( genericParam) ,
3611- message:
3612- " Generic parameter ' \( paramName) ' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JSFunction. "
3613- )
3614- )
3693+ switch parent. parseGenericParameterConstraints ( genericParam, attributeName: " @JSFunction " ) {
3694+ case . failed( let message) :
3695+ errors. append ( DiagnosticError ( node: Syntax ( genericParam) , message: message) )
36153696 return nil
3697+ case . parsed( let genericParameter) :
3698+ genericParameters. append ( genericParameter)
36163699 }
3617- genericParameterNames. append ( paramName)
36183700 }
36193701 }
36203702 if genericWhereClause != nil {
@@ -3626,7 +3708,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
36263708 )
36273709 return nil
36283710 }
3629- return genericParameterNames
3711+ return genericParameters
36303712 }
36313713
36323714 private func parseConstructor(
@@ -3643,14 +3725,15 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
36433725 return nil
36443726 }
36453727 guard
3646- let genericParameterNames = parseGenericParameterNames (
3728+ let genericParameters = parseGenericParameters (
36473729 genericParameterClause: initializer. genericParameterClause,
36483730 genericWhereClause: initializer. genericWhereClause,
36493731 node: Syntax ( initializer)
36503732 )
36513733 else {
36523734 return nil
36533735 }
3736+ let genericParameterNames = genericParameters. map ( \. name)
36543737 if !genericParameterNames. isEmpty && effects. isAsync {
36553738 errors. append (
36563739 DiagnosticError (
@@ -3683,7 +3766,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
36833766 return ImportedConstructorSkeleton (
36843767 parameters: parameters,
36853768 accessLevel: accessLevel,
3686- genericParameters: genericParameterNames . isEmpty ? nil : genericParameterNames
3769+ genericParameters: genericParameters . isEmpty ? nil : genericParameters
36873770 )
36883771 }
36893772
@@ -3702,14 +3785,15 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
37023785 }
37033786
37043787 guard
3705- let genericParameterNames = parseGenericParameterNames (
3788+ let genericParameters = parseGenericParameters (
37063789 genericParameterClause: node. genericParameterClause,
37073790 genericWhereClause: node. genericWhereClause,
37083791 node: Syntax ( node)
37093792 )
37103793 else {
37113794 return nil
37123795 }
3796+ let genericParameterNames = genericParameters. map ( \. name)
37133797
37143798 let baseName = SwiftToSkeleton . normalizeIdentifier ( node. name. text)
37153799 let extractedJSName = extractJSName ( from: jsFunction)
@@ -3773,7 +3857,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
37733857 effects: effects,
37743858 documentation: nil ,
37753859 accessLevel: accessLevel,
3776- genericParameters: genericParameterNames . isEmpty ? nil : genericParameterNames
3860+ genericParameters: genericParameters . isEmpty ? nil : genericParameters
37773861 )
37783862 }
37793863
0 commit comments