Skip to content

Commit ec4cef3

Browse files
committed
BridgeJS: Support @js protocol constraints on generic imports
An imported generic @JSFunction can now compose one or more @js protocols with the bridgeable bound, in any order: @JSFunction func fetchNode<T: BridgedSwiftGenericBridgeable & GraphNode>(_ key: String) throws(JSException) -> T The Swift compiler enforces the constraint at every call site, so no runtime check is needed on imports; values still cross as the concrete type. The skeleton's generic parameters grow from bare name strings to {name, constraints} records (with tolerant decoding of the old string shape, since dependency targets check in generated skeletons), and the generated d.ts carries the constraint as 'T extends GraphNode', referencing the protocol's generated interface. Every protocol in the composition must be a @js protocol declared in the module or in an indexed dependency; anything else — a missing bridgeable bound, an unknown or non-@js protocol — is rejected with a diagnostic that spells out the expected composition.
1 parent 200712b commit ec4cef3

17 files changed

Lines changed: 2225 additions & 151 deletions

File tree

‎Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public struct ImportTS {
381381
parameters: [Parameter],
382382
returnType: BridgeType,
383383
effects: Effects,
384-
genericParameters: [String] = []
384+
genericParameters: [GenericParameter] = []
385385
) -> DeclSyntax {
386386
let printer = CodeFragmentPrinter()
387387
let signature = SwiftSignatureBuilder.buildFunctionSignature(
@@ -393,7 +393,11 @@ public struct ImportTS {
393393
let genericClause =
394394
genericParameters.isEmpty
395395
? ""
396-
: "<" + genericParameters.map { "\($0): BridgedSwiftGenericBridgeable" }.joined(separator: ", ")
396+
: "<"
397+
+ genericParameters.map { genericParameter in
398+
let constraints = ["BridgedSwiftGenericBridgeable"] + genericParameter.constraints
399+
return "\(genericParameter.name): \(constraints.joined(separator: " & "))"
400+
}.joined(separator: ", ")
397401
+ ">"
398402
printer.write("func \(name.backtickIfNeeded())\(genericClause)\(signature) {")
399403
printer.indent {
@@ -465,7 +469,7 @@ public struct ImportTS {
465469
parameters: function.parameters,
466470
returnType: function.returnType,
467471
effects: function.effects,
468-
genericParameters: function.genericParameterNames
472+
genericParameters: function.genericParameters ?? []
469473
)
470474
.with(\.leadingTrivia, Self.renderDocumentation(documentation: function.documentation))
471475
]
@@ -496,7 +500,7 @@ public struct ImportTS {
496500
parameters: [selfParameter] + method.parameters,
497501
returnType: method.returnType,
498502
effects: method.effects,
499-
genericParameters: method.genericParameterNames
503+
genericParameters: method.genericParameters ?? []
500504
)
501505
]
502506
}
@@ -522,7 +526,7 @@ public struct ImportTS {
522526
parameters: method.parameters,
523527
returnType: method.returnType,
524528
effects: method.effects,
525-
genericParameters: method.genericParameterNames
529+
genericParameters: method.genericParameters ?? []
526530
)
527531
]
528532
}
@@ -548,7 +552,7 @@ public struct ImportTS {
548552
parameters: constructor.parameters,
549553
returnType: .jsObject(nil),
550554
effects: effects,
551-
genericParameters: constructor.genericParameterNames
555+
genericParameters: constructor.genericParameters ?? []
552556
)
553557
]
554558
}

‎Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift‎

Lines changed: 116 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import BridgeJSUtilities
77
import 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.
1118
enum 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

9911072
private 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

‎Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,7 @@ public struct BridgeJSLink {
16071607
// Add methods
16081608
for method in type.methods {
16091609
let methodName = method.resolvedJSName
1610-
let genericClause = renderGenericClause(method.genericParameterNames)
1610+
let genericClause = renderGenericClause(method.genericParameters ?? [])
16111611
let methodSignature =
16121612
"\(renderTSPropertyName(methodName))\(genericClause)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
16131613
printer.write(methodSignature)
@@ -1948,8 +1948,14 @@ public struct BridgeJSLink {
19481948
return "(\(parameterSignatures.joined(separator: ", "))): \(returnTypeWithEffect)"
19491949
}
19501950

1951-
private func renderGenericClause(_ genericParameterNames: [String]) -> String {
1952-
genericParameterNames.isEmpty ? "" : "<\(genericParameterNames.joined(separator: ", "))>"
1951+
private func renderGenericClause(_ genericParameters: [GenericParameter]) -> String {
1952+
guard !genericParameters.isEmpty else { return "" }
1953+
let renderedParameters = genericParameters.map { genericParameter in
1954+
genericParameter.constraints.isEmpty
1955+
? genericParameter.name
1956+
: "\(genericParameter.name) extends \(genericParameter.constraints.joined(separator: " & "))"
1957+
}
1958+
return "<\(renderedParameters.joined(separator: ", "))>"
19531959
}
19541960

19551961
/// The d.ts member signature of an exported function; generic functions
@@ -1972,7 +1978,7 @@ public struct BridgeJSLink {
19721978
for (genericName, tokenName) in zip(genericNames, tokenNames) {
19731979
parameterSignatures.append("\(tokenName): BridgeType<\(genericName)>")
19741980
}
1975-
let genericClause = renderGenericClause(genericNames)
1981+
let genericClause = renderGenericClause(function.genericParameters ?? [])
19761982
let returnSignature = resolveTypeScriptType(function.returnType)
19771983
return
19781984
"\(function.resolvedJSName)\(genericClause)(\(parameterSignatures.joined(separator: ", "))): \(returnSignature)"
@@ -3919,10 +3925,9 @@ extension BridgeJSLink {
39193925
returnType: function.returnType,
39203926
intrinsicRegistry: intrinsicRegistry
39213927
)
3922-
let genericParameters = function.genericParameterNames
39233928
try thunkBuilder.liftParametersAndGenericTypeIds(
39243929
function.parameters,
3925-
genericParameters: genericParameters
3930+
genericParameters: function.genericParameterNames
39263931
)
39273932
let jsName = function.resolvedJSName
39283933
let calleeExpr = try importedModuleRegistry.memberExpression(
@@ -3934,7 +3939,7 @@ extension BridgeJSLink {
39343939
try thunkBuilder.call(calleeExpr: calleeExpr)
39353940
let funcLines = thunkBuilder.renderFunction(name: function.abiName(context: nil))
39363941
if function.from == nil {
3937-
let genericClause = renderGenericClause(genericParameters)
3942+
let genericClause = renderGenericClause(function.genericParameters ?? [])
39383943
importObjectBuilder.appendDts(
39393944
[
39403945
"\(renderTSPropertyName(jsName))\(genericClause)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
@@ -4027,14 +4032,14 @@ extension BridgeJSLink {
40274032
dtsPrinter.indent {
40284033
if let constructor = type.constructor {
40294034
let returnType = BridgeType.jsObject(type.name)
4030-
let genericClause = renderGenericClause(constructor.genericParameterNames)
4035+
let genericClause = renderGenericClause(constructor.genericParameters ?? [])
40314036
dtsPrinter.write(
40324037
"new\(genericClause)\(renderTSSignature(parameters: constructor.parameters, returnType: returnType, effects: Effects(isAsync: false, isThrows: false)));"
40334038
)
40344039
}
40354040
for method in type.staticMethods {
40364041
let methodName = method.resolvedJSName
4037-
let genericClause = renderGenericClause(method.genericParameterNames)
4042+
let genericClause = renderGenericClause(method.genericParameters ?? [])
40384043
let signature =
40394044
"\(renderTSPropertyName(methodName))\(genericClause)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
40404045
dtsPrinter.write(signature)
@@ -4145,10 +4150,9 @@ extension BridgeJSLink {
41454150
intrinsicRegistry: intrinsicRegistry
41464151
)
41474152
thunkBuilder.liftSelf()
4148-
let genericParameters = method.genericParameterNames
41494153
try thunkBuilder.liftParametersAndGenericTypeIds(
41504154
method.parameters,
4151-
genericParameters: genericParameters
4155+
genericParameters: method.genericParameterNames
41524156
)
41534157

41544158
try thunkBuilder.callMethod(name: method.resolvedJSName)

0 commit comments

Comments
 (0)