expo-modules-macros 0.0.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +389 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- package/package.json +22 -5
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Member macro applied to a `SharedObject` subclass. Scans the class body for
|
|
7
|
+
declarations marked with `@JS` and synthesizes `_synthesizedClassDefinition()`,
|
|
8
|
+
returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
|
|
9
|
+
|
|
10
|
+
@SharedObject
|
|
11
|
+
final class Cache: SharedObject {
|
|
12
|
+
@JS
|
|
13
|
+
init(name: String) { self.name = name }
|
|
14
|
+
|
|
15
|
+
@JS
|
|
16
|
+
func get(_ key: String) -> String? { ... }
|
|
17
|
+
|
|
18
|
+
@JS
|
|
19
|
+
var size: Int { 42 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
|
|
23
|
+
definition into the module's exposed surface.
|
|
24
|
+
*/
|
|
25
|
+
public struct SharedObjectMacro: MemberMacro {
|
|
26
|
+
public static func expansion(
|
|
27
|
+
of node: AttributeSyntax,
|
|
28
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
29
|
+
conformingTo protocols: [TypeSyntax],
|
|
30
|
+
in context: some MacroExpansionContext
|
|
31
|
+
) throws -> [DeclSyntax] {
|
|
32
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
33
|
+
throw MacroExpansionErrorMessage("@SharedObject can only be applied to a class")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
guard inheritsFromSharedObject(classDecl) else {
|
|
37
|
+
throw MacroExpansionErrorMessage(
|
|
38
|
+
"@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration.")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let typeName = classDecl.name.text
|
|
42
|
+
let jsName = jsNameArgument(of: node) ?? typeName
|
|
43
|
+
|
|
44
|
+
// `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
|
|
45
|
+
// the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
|
|
46
|
+
// rather than described with a `Function(...)` / `Property(...)` / `Constructor { … }` DSL entry, so
|
|
47
|
+
// they're collected here instead of appended to the `Class` block. The block keeps only non-`@JS`
|
|
48
|
+
// definitions (none are collected today), so it's empty when every member is `@JS`.
|
|
49
|
+
//
|
|
50
|
+
// Members split by the `static`/`class` modifier onto two different JS objects: instance members
|
|
51
|
+
// decorate the prototype (receiver recovered from JS `this`), static members decorate the
|
|
52
|
+
// constructor (called on the metatype). A JS instance and static member may share a name without
|
|
53
|
+
// colliding: they live on different objects.
|
|
54
|
+
let entries: [String] = []
|
|
55
|
+
var instanceFunctions: [JSFunction] = []
|
|
56
|
+
var instanceProperties: [JSProperty] = []
|
|
57
|
+
var staticFunctions: [JSFunction] = []
|
|
58
|
+
var staticProperties: [JSProperty] = []
|
|
59
|
+
var constructor: JSConstructor?
|
|
60
|
+
|
|
61
|
+
for member in classDecl.memberBlock.members {
|
|
62
|
+
let decl = member.decl
|
|
63
|
+
|
|
64
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
65
|
+
initDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
66
|
+
if constructor != nil {
|
|
67
|
+
throw MacroExpansionErrorMessage(
|
|
68
|
+
"@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor.")
|
|
69
|
+
}
|
|
70
|
+
constructor = JSConstructor(initDecl: initDecl)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
75
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
76
|
+
let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
|
|
77
|
+
if isTypeLevel(funcDecl.modifiers) {
|
|
78
|
+
staticFunctions.append(function)
|
|
79
|
+
} else {
|
|
80
|
+
instanceFunctions.append(function)
|
|
81
|
+
}
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
86
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
87
|
+
let collected = collectProperties(varDecl: varDecl, attribute: attribute)
|
|
88
|
+
if isTypeLevel(varDecl.modifiers) {
|
|
89
|
+
staticProperties.append(contentsOf: collected)
|
|
90
|
+
} else {
|
|
91
|
+
instanceProperties.append(contentsOf: collected)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let lines = entries.map { " \($0)" }.joined(separator: "\n")
|
|
97
|
+
let body = entries.isEmpty
|
|
98
|
+
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
|
99
|
+
: " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
|
|
100
|
+
|
|
101
|
+
var emitted: [DeclSyntax] = [
|
|
102
|
+
"""
|
|
103
|
+
public static func _synthesizedClassDefinition() -> ClassDefinition {
|
|
104
|
+
\(raw: body)
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
// Direct JSI binding, split by which JS object each member decorates:
|
|
110
|
+
// `_decorateSharedObject(prototype:)` binds instance `@JS func`/`var`s (unwrapping the per-call receiver from
|
|
111
|
+
// `this`); `_decorateSharedObject(constructor:)` binds `static`/`class` ones (called on the metatype); and
|
|
112
|
+
// `_constructSharedObject` builds an instance from the `@JS init` arguments. Each is emitted only
|
|
113
|
+
// when it has something to bind.
|
|
114
|
+
if !instanceFunctions.isEmpty || !instanceProperties.isEmpty {
|
|
115
|
+
emitted.append(
|
|
116
|
+
buildDecorateSharedObjectPhase(
|
|
117
|
+
phase: .prototype, functions: instanceFunctions, properties: instanceProperties,
|
|
118
|
+
typeName: typeName))
|
|
119
|
+
}
|
|
120
|
+
if !staticFunctions.isEmpty || !staticProperties.isEmpty {
|
|
121
|
+
emitted.append(
|
|
122
|
+
buildDecorateSharedObjectPhase(
|
|
123
|
+
phase: .constructor, functions: staticFunctions, properties: staticProperties,
|
|
124
|
+
typeName: typeName))
|
|
125
|
+
}
|
|
126
|
+
if let constructor {
|
|
127
|
+
emitted.append(constructor.buildConstructor(typeName: typeName))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return emitted
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
extension SharedObjectMacro: MemberAttributeMacro {
|
|
135
|
+
public static func expansion(
|
|
136
|
+
of node: AttributeSyntax,
|
|
137
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
138
|
+
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
139
|
+
in context: some MacroExpansionContext
|
|
140
|
+
) throws -> [AttributeSyntax] {
|
|
141
|
+
// `@Event(sync: true)` members are stamped alongside `@JS` ones: a sync event dispatches
|
|
142
|
+
// inline, so the isolation forces its call site onto the JS thread. Async events (the
|
|
143
|
+
// default) stay unstamped — their `emit` schedules onto the JS thread itself.
|
|
144
|
+
guard memberHasJSAttribute(member) || isSyncEventMember(member) else {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
// `@JS(.concurrent)` opts the member out of the JS-thread stamp and onto the concurrent pool.
|
|
148
|
+
if isConcurrentJSMember(member) {
|
|
149
|
+
return ["@concurrent"]
|
|
150
|
+
}
|
|
151
|
+
guard shouldStampJavaScriptActor(on: member, enclosedBy: declaration) else {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
return ["@JavaScriptActor"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - Inheritance check
|
|
159
|
+
|
|
160
|
+
private func inheritsFromSharedObject(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
161
|
+
return inheritsFromAny(classDecl, names: ["SharedObject"])
|
|
162
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
|
|
4
|
+
/// eventual rename (this is a placeholder name) is a single edit, and shared by the macros whose
|
|
5
|
+
/// generated conversions go through the dynamic-type API (`@Record`, `@Event`).
|
|
6
|
+
internal let jsConvertibleProtocolName = "AnyArgument"
|
|
7
|
+
|
|
8
|
+
/// The constraint a `@JS` boundary type must satisfy, by direction: an argument (and a settable
|
|
9
|
+
/// property's incoming value) is decoded, so it must be `JavaScriptDecodable`; a return value (and a
|
|
10
|
+
/// property's outgoing value) is encoded, so it must be `JavaScriptEncodable`. Asserting each
|
|
11
|
+
/// direction on its own keeps a decode-only or encode-only type from being over-constrained, and
|
|
12
|
+
/// surfaces a missing conformance as a clear "requires that '…' conform to …" diagnostic on the
|
|
13
|
+
/// member instead of an opaque error inside the generated closure.
|
|
14
|
+
internal let javaScriptDecodableProtocolName = "JavaScriptDecodable"
|
|
15
|
+
internal let javaScriptEncodableProtocolName = "JavaScriptEncodable"
|
|
16
|
+
|
|
17
|
+
/// The constraint a `@Record` property type must satisfy: it must be both `AnyArgument` (the
|
|
18
|
+
/// `from(dictionary:)` / `toDictionary(appContext:)` paths still convert native `Any` through the
|
|
19
|
+
/// dynamic-type API) and `JavaScriptDecodable & JavaScriptEncodable` (the `from(object:)` /
|
|
20
|
+
/// `toObject(appContext:)` paths convert JS values through `decode`/`encode`). A record field has to
|
|
21
|
+
/// support both directions, so the assertion requires the intersection.
|
|
22
|
+
///
|
|
23
|
+
/// The exception is a property whose type mentions `Any` (`[String: Any]?`, `[Any]`, …): it can't
|
|
24
|
+
/// conform to the JS codable protocols, so the JS-value paths convert it through the `JavaScriptValue`
|
|
25
|
+
/// free-form methods, and it's asserted against `jsConvertibleProtocolName` alone for the dictionary
|
|
26
|
+
/// paths.
|
|
27
|
+
internal let recordFieldProtocolName = "AnyArgument & JavaScriptDecodable & JavaScriptEncodable"
|
|
28
|
+
|
|
29
|
+
/// The constraint a `@Union` case payload type must satisfy: the union decodes by trying each payload's
|
|
30
|
+
/// `decode` and encodes through the matching payload's `encode`, so every alternative must convert in
|
|
31
|
+
/// both directions. Unlike a `@Record` field, a union never crosses the native-`Any` dictionary path, so
|
|
32
|
+
/// `AnyArgument` is not required.
|
|
33
|
+
internal let unionPayloadProtocolName = "JavaScriptDecodable & JavaScriptEncodable"
|
|
34
|
+
|
|
35
|
+
/// The protocol a type must conform to for `self.emit(event:…)` to resolve; core conforms
|
|
36
|
+
/// `BaseModule` and `SharedObject` to it. Asserted by `@Event` so attaching it to a type that can't
|
|
37
|
+
/// emit fails with a conformance diagnostic instead of an opaque "no member 'emit'" error.
|
|
38
|
+
internal let eventEmitterProtocolName = "EventEmitter"
|
|
39
|
+
|
|
40
|
+
/// Types we never assert because they're statically known to conform: the JS primitives. Asserting
|
|
41
|
+
/// them would only add noise to the expansion.
|
|
42
|
+
private let knownConformingPrimitives: Set<String> = ["Bool", "Int", "Double", "String"]
|
|
43
|
+
|
|
44
|
+
/// One member's worth of conformance assertion: a name (the member it stands for) and the declared
|
|
45
|
+
/// types crossing the JS boundary for it. The name surfaces verbatim in the compiler's conformance
|
|
46
|
+
/// diagnostic ("local function '<name>' requires that '<Type>' conform to …"), so it identifies the
|
|
47
|
+
/// offending member in the error message on top of the location pointing at the user's declaration.
|
|
48
|
+
internal struct ConformanceAssertion {
|
|
49
|
+
let name: String
|
|
50
|
+
/// Declared types as written. Composed types (`[Int]`, `String?`, …) are kept verbatim — their
|
|
51
|
+
/// conditional conformances transitively constrain the elements.
|
|
52
|
+
let types: [String]
|
|
53
|
+
/// The constraint for this member's types, overriding the one passed to `typeConformanceAssertions`.
|
|
54
|
+
/// `@Record` uses it to assert only `AnyArgument` for a property that mentions `Any`, which can't be
|
|
55
|
+
/// `JavaScriptDecodable & JavaScriptEncodable`.
|
|
56
|
+
var constraint: String? = nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// A directional conformance-assertion peer for a `@JS` member: a never-called `private func` whose
|
|
60
|
+
/// body asserts each argument type is `JavaScriptDecodable` and the return/getter type is
|
|
61
|
+
/// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
|
|
62
|
+
/// encode on the way out). The check is a single member-named nested helper with one `A0…` generic
|
|
63
|
+
/// parameter per non-primitive argument and a single `Return` parameter for the value, called once with
|
|
64
|
+
/// every type's metatype; naming the helper after the member puts the member in the compiler's
|
|
65
|
+
/// diagnostic, and the per-slot constraint reports the offending type against the protocol for *its*
|
|
66
|
+
/// direction.
|
|
67
|
+
///
|
|
68
|
+
/// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
|
|
69
|
+
/// the single return type (or a property's value type on read), or `nil` when there's none. Primitives
|
|
70
|
+
/// are dropped from each; when nothing is left in either, returns `nil` so the caller emits no peer.
|
|
71
|
+
/// `isStatic` mirrors a `static`/`class` member so the peer sits in the right metatype context.
|
|
72
|
+
internal func directionalConformanceAssertion(
|
|
73
|
+
name: String,
|
|
74
|
+
decodableTypes: [String],
|
|
75
|
+
encodableType: String?,
|
|
76
|
+
isStatic: Bool
|
|
77
|
+
) -> DeclSyntax? {
|
|
78
|
+
let decodables = distinctAssertableTypes(decodableTypes)
|
|
79
|
+
let encodable = encodableType.flatMap(assertableBoundaryType)
|
|
80
|
+
guard !decodables.isEmpty || encodable != nil else {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
|
|
85
|
+
// constrained `JavaScriptDecodable` per argument, and a single `Return` slot constrained
|
|
86
|
+
// `JavaScriptEncodable` for the return/getter value (there's only ever one, so it isn't indexed).
|
|
87
|
+
var parameters: [String] = []
|
|
88
|
+
var typeParameters: [String] = []
|
|
89
|
+
var arguments: [String] = []
|
|
90
|
+
for (index, type) in decodables.enumerated() {
|
|
91
|
+
parameters.append("A\(index): \(javaScriptDecodableProtocolName)")
|
|
92
|
+
typeParameters.append("_: A\(index).Type")
|
|
93
|
+
arguments.append("\(type).self")
|
|
94
|
+
}
|
|
95
|
+
if let encodable {
|
|
96
|
+
parameters.append("Return: \(javaScriptEncodableProtocolName)")
|
|
97
|
+
typeParameters.append("_: Return.Type")
|
|
98
|
+
arguments.append("\(encodable).self")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
|
|
102
|
+
let call = "\(name)(\(arguments.joined(separator: ", ")))"
|
|
103
|
+
let staticKeyword = isStatic ? "static " : ""
|
|
104
|
+
return """
|
|
105
|
+
private \(raw: staticKeyword)func _assertTypesConformance_\(raw: name)() {
|
|
106
|
+
\(raw: helper)
|
|
107
|
+
\(raw: call)
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// One conformance-assertion peer covering several members' assertions at once — used by `@Record`,
|
|
113
|
+
/// which folds every property into a single `_assertTypesConformance()` rather than emitting a peer
|
|
114
|
+
/// per property. Each assertion keeps its own named nested helper, so the per-member naming in the
|
|
115
|
+
/// diagnostic is preserved even though they share one peer.
|
|
116
|
+
///
|
|
117
|
+
/// Returns `nil` when no assertion has anything left to verify (all primitives / empty), so the
|
|
118
|
+
/// caller emits nothing.
|
|
119
|
+
internal func typeConformanceAssertions(
|
|
120
|
+
for assertions: [ConformanceAssertion],
|
|
121
|
+
constraint: String = jsConvertibleProtocolName
|
|
122
|
+
) -> DeclSyntax? {
|
|
123
|
+
let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: $0.constraint ?? constraint) }
|
|
124
|
+
guard !bodies.isEmpty else {
|
|
125
|
+
return nil
|
|
126
|
+
}
|
|
127
|
+
return """
|
|
128
|
+
private func _assertTypesConformance() {
|
|
129
|
+
\(raw: bodies.joined(separator: "\n"))
|
|
130
|
+
}
|
|
131
|
+
"""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// The type to assert for a boundary type as written, or `nil` when nothing needs asserting: an
|
|
135
|
+
/// exactly-free-form type (handled by the binding's `decodeAny…` reroute) and a known-conforming
|
|
136
|
+
/// primitive both return `nil`; everything else is unwrapped of trailing optionals and asserted. An
|
|
137
|
+
/// *optional* free-form (`[String: Any]?`) isn't rerouted and doesn't conform, so it's kept and
|
|
138
|
+
/// asserted for a clean diagnostic. Shared with `@Event`.
|
|
139
|
+
internal func assertableBoundaryType(_ type: String) -> String? {
|
|
140
|
+
guard !isFreeFormBoundaryType(type) else {
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
let unwrapped = unwrappedOptional(type)
|
|
144
|
+
return knownConformingPrimitives.contains(unwrapped) ? nil : unwrapped
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// The assertion's body fragment: a nested generic helper named after the member, plus one call per
|
|
148
|
+
/// distinct non-primitive type. Nesting the helper keeps the constraint entirely local — no shared
|
|
149
|
+
/// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
|
|
150
|
+
/// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
|
|
151
|
+
/// a known-conforming primitive or the list was empty.
|
|
152
|
+
private func conformanceAssertionBody(
|
|
153
|
+
_ assertion: ConformanceAssertion,
|
|
154
|
+
constraint: String = jsConvertibleProtocolName
|
|
155
|
+
) -> String? {
|
|
156
|
+
let distinct = distinctAssertableTypes(assertion.types)
|
|
157
|
+
guard !distinct.isEmpty else {
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var lines = ["func \(assertion.name)<T: \(constraint)>(_: T.Type) {}"]
|
|
162
|
+
lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
|
|
163
|
+
return lines.joined(separator: "\n")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// The distinct types to assert from a list, mapping each through `assertableBoundaryType` (which
|
|
167
|
+
/// drops what needs no assertion) and deduping in first-seen order.
|
|
168
|
+
private func distinctAssertableTypes(_ types: [String]) -> [String] {
|
|
169
|
+
var seen: Set<String> = []
|
|
170
|
+
var distinct: [String] = []
|
|
171
|
+
for type in types.compactMap(assertableBoundaryType) where seen.insert(type).inserted {
|
|
172
|
+
distinct.append(type)
|
|
173
|
+
}
|
|
174
|
+
return distinct
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Strips every trailing optional marker (`?`/`!`) so the assertion targets the core wrapped type.
|
|
178
|
+
/// `Optional<W>: AnyArgument` holds exactly when `W: AnyArgument` (and `T!` is just `T?`), so each
|
|
179
|
+
/// layer is conformance-equivalent to its wrapped type. Asserting the core gives a cleaner diagnostic
|
|
180
|
+
/// (the direct `requires that '<type>' conform`, not the conditional-conformance phrasing through
|
|
181
|
+
/// `Optional`) and sidesteps that `T!.self` is invalid in metatype position. Only *trailing* markers
|
|
182
|
+
/// are stripped, so `[Int?]` keeps its inner `?`; a longhand `Optional<W>` isn't peeled but is still
|
|
183
|
+
/// asserted whole, which remains correct.
|
|
184
|
+
private func unwrappedOptional(_ type: String) -> String {
|
|
185
|
+
var result = Substring(type)
|
|
186
|
+
while result.hasSuffix("?") || result.hasSuffix("!") {
|
|
187
|
+
result = result.dropLast()
|
|
188
|
+
}
|
|
189
|
+
return String(result)
|
|
190
|
+
}
|