expo-modules-macros 0.21.0 → 0.23.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/workflows/linux.yml +8 -5
- package/.github/workflows/publish.yml +26 -11
- package/.github/workflows/swift-format.yml +79 -0
- package/.github/workflows/swift.yml +7 -4
- package/.github/workflows/windows.yml +8 -5
- package/AGENTS.md +16 -9
- package/README.md +20 -5
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +3 -3
- package/apple/Package.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +144 -30
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +184 -48
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
- package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +20 -4
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/index.d.ts +1 -1
- package/build/index.js +3 -3
- package/build/types.d.ts +32 -2
- package/build/types.js +1 -1
- package/package.json +8 -5
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
6
|
-
*/
|
|
3
|
+
/// Reads the first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork".
|
|
4
|
+
/// Returns nil if the attribute has no arguments or the first argument is not a string literal.
|
|
7
5
|
internal func jsNameArgument(of attribute: AttributeSyntax) -> String? {
|
|
8
6
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
9
7
|
let first = args.first,
|
|
10
8
|
let str = first.expression.as(StringLiteralExprSyntax.self),
|
|
11
|
-
let segment = str.segments.first?.as(StringSegmentSyntax.self)
|
|
9
|
+
let segment = str.segments.first?.as(StringSegmentSyntax.self)
|
|
10
|
+
else {
|
|
12
11
|
return nil
|
|
13
12
|
}
|
|
14
13
|
return segment.content.text
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
|
|
20
|
-
*/
|
|
16
|
+
/// Reads a labeled boolean-literal argument of an attribute, e.g. `@Event(sync: true)` -> true.
|
|
17
|
+
/// Returns nil if the attribute has no argument with that label or its value isn't a boolean literal.
|
|
21
18
|
internal func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool? {
|
|
22
19
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
23
20
|
return nil
|
|
@@ -44,7 +41,8 @@ internal func hasJSOption(_ attribute: AttributeSyntax, named option: String) ->
|
|
|
44
41
|
return true
|
|
45
42
|
}
|
|
46
43
|
if let array = arg.expression.as(ArrayExprSyntax.self),
|
|
47
|
-
array.elements.contains(where: { namesOption($0.expression, option) })
|
|
44
|
+
array.elements.contains(where: { namesOption($0.expression, option) })
|
|
45
|
+
{
|
|
48
46
|
return true
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -69,6 +67,64 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
69
67
|
return hasJSOption(attribute, named: "concurrent")
|
|
70
68
|
}
|
|
71
69
|
|
|
70
|
+
/// The `.withAsync("name")` option of a `@JS` attribute, e.g. `@JS(.withAsync("loadAsync"))` or
|
|
71
|
+
/// `@JS("load", [.withAsync("loadAsync")])`, or `nil` when the attribute doesn't list it. When the
|
|
72
|
+
/// option is listed more than once, the first one counts and `@JS` reports the others.
|
|
73
|
+
internal func asyncVariantOption(of attribute: AttributeSyntax) -> FunctionCallExprSyntax? {
|
|
74
|
+
return asyncVariantOptions(of: attribute).first
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Every `.withAsync(...)` option of a `@JS` attribute, in source order. Like `hasJSOption`, it scans
|
|
78
|
+
/// the arguments and the elements of an array literal.
|
|
79
|
+
internal func asyncVariantOptions(of attribute: AttributeSyntax) -> [FunctionCallExprSyntax] {
|
|
80
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
return args.flatMap { arg -> [FunctionCallExprSyntax] in
|
|
84
|
+
if let array = arg.expression.as(ArrayExprSyntax.self) {
|
|
85
|
+
return array.elements.compactMap { asyncOptionCall($0.expression) }
|
|
86
|
+
}
|
|
87
|
+
return asyncOptionCall(arg.expression).map { [$0] } ?? []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The expression as a `.withAsync(...)` call (or `JSOptions.withAsync(...)`), or `nil` for anything
|
|
92
|
+
/// else.
|
|
93
|
+
private func asyncOptionCall(_ expression: ExprSyntax) -> FunctionCallExprSyntax? {
|
|
94
|
+
guard let call = expression.as(FunctionCallExprSyntax.self),
|
|
95
|
+
let member = call.calledExpression.as(MemberAccessExprSyntax.self),
|
|
96
|
+
member.declName.baseName.text == "withAsync"
|
|
97
|
+
else {
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
return call
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The JavaScript name a `.withAsync("name")` option gives the promise-returning variant, with the
|
|
104
|
+
/// token of its string literal, where a name collision is reported. `nil` when the argument isn't a
|
|
105
|
+
/// single plain string literal, which `@JS` reports.
|
|
106
|
+
internal func asyncVariantName(of option: FunctionCallExprSyntax) -> (name: String, token: TokenSyntax)? {
|
|
107
|
+
guard option.arguments.count == 1,
|
|
108
|
+
let argument = option.arguments.first,
|
|
109
|
+
argument.label == nil,
|
|
110
|
+
let literal = argument.expression.as(StringLiteralExprSyntax.self),
|
|
111
|
+
literal.segments.count == 1,
|
|
112
|
+
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
113
|
+
else {
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
return (segment.content.text, segment.content)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// True if the `@JS`-marked declaration lists `.withAsync("name")`. Such a function is left
|
|
120
|
+
/// unstamped, because its promise-returning variant calls it on the concurrent pool.
|
|
121
|
+
internal func hasAsyncVariant(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
122
|
+
guard let attribute = memberAttributes(of: decl).firstAttribute(named: "JS") else {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
return asyncVariantOption(of: attribute) != nil
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
|
|
73
129
|
/// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
|
|
74
130
|
internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
@@ -129,12 +185,82 @@ internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
|
129
185
|
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
130
186
|
identifier.name.text == "Optional",
|
|
131
187
|
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
132
|
-
case .type(let wrapped) = argument.argument
|
|
188
|
+
case .type(let wrapped) = argument.argument
|
|
189
|
+
{
|
|
133
190
|
return wrapped
|
|
134
191
|
}
|
|
135
192
|
return nil
|
|
136
193
|
}
|
|
137
194
|
|
|
195
|
+
/// The element type of `[T]` or `Array<T>`, or `nil` for any other type.
|
|
196
|
+
internal func arrayElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
197
|
+
if let array = type.as(ArrayTypeSyntax.self) {
|
|
198
|
+
return array.element
|
|
199
|
+
}
|
|
200
|
+
guard let arguments = genericArguments(of: type, named: "Array"), arguments.count == 1 else {
|
|
201
|
+
return nil
|
|
202
|
+
}
|
|
203
|
+
return arguments[0]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// The value type of `[String: V]` or `Dictionary<String, V>`, or `nil` for any other type, including a
|
|
207
|
+
/// dictionary with a different key type.
|
|
208
|
+
internal func stringKeyedDictionaryValueType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
209
|
+
if let dictionary = type.as(DictionaryTypeSyntax.self) {
|
|
210
|
+
return isStringType(dictionary.key) ? dictionary.value : nil
|
|
211
|
+
}
|
|
212
|
+
guard let arguments = genericArguments(of: type, named: "Dictionary"), arguments.count == 2 else {
|
|
213
|
+
return nil
|
|
214
|
+
}
|
|
215
|
+
return isStringType(arguments[0]) ? arguments[1] : nil
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// The element type of `Set<T>`, or `nil` for any other type.
|
|
219
|
+
internal func setElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
220
|
+
guard let arguments = genericArguments(of: type, named: "Set"), arguments.count == 1 else {
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
return arguments[0]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// The type arguments of a generic type spelled `<name><…>` (or `Swift.<name><…>`), or `nil` when the
|
|
227
|
+
/// type has another name or no type arguments.
|
|
228
|
+
private func genericArguments(of type: TypeSyntax, named name: String) -> [TypeSyntax]? {
|
|
229
|
+
let clause: GenericArgumentClauseSyntax?
|
|
230
|
+
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == name {
|
|
231
|
+
clause = identifier.genericArgumentClause
|
|
232
|
+
} else if let member = type.as(MemberTypeSyntax.self),
|
|
233
|
+
member.name.text == name,
|
|
234
|
+
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
235
|
+
{
|
|
236
|
+
clause = member.genericArgumentClause
|
|
237
|
+
} else {
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
guard let clause else {
|
|
241
|
+
return nil
|
|
242
|
+
}
|
|
243
|
+
var types: [TypeSyntax] = []
|
|
244
|
+
for argument in clause.arguments {
|
|
245
|
+
guard case .type(let type) = argument.argument else {
|
|
246
|
+
return nil
|
|
247
|
+
}
|
|
248
|
+
types.append(type)
|
|
249
|
+
}
|
|
250
|
+
return types
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// True when the type is `String` or `Swift.String`.
|
|
254
|
+
private func isStringType(_ type: TypeSyntax) -> Bool {
|
|
255
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
256
|
+
return identifier.name.text == "String" && identifier.genericArgumentClause == nil
|
|
257
|
+
}
|
|
258
|
+
if let member = type.as(MemberTypeSyntax.self), member.name.text == "String" {
|
|
259
|
+
return member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
260
|
+
}
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
138
264
|
/// True when the modifiers make the member type-level (`static` or `class`). A shared object routes
|
|
139
265
|
/// such members to its constructor (JS-static) and instance members to its prototype; the `@JS`
|
|
140
266
|
/// conformance assertion also uses this to emit its peer in the matching metatype context.
|
|
@@ -144,6 +270,12 @@ internal func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
144
270
|
}
|
|
145
271
|
}
|
|
146
272
|
|
|
273
|
+
/// True for a `lazy` property. A lazy `@JS` property is read-only from JS: its getter stores the
|
|
274
|
+
/// first value as a data property on the JS object, which a setter couldn't write through.
|
|
275
|
+
internal func isLazy(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
276
|
+
return modifiers.contains { $0.name.tokenKind == .keyword(.lazy) }
|
|
277
|
+
}
|
|
278
|
+
|
|
147
279
|
/// True if a trailing occurrence of this parameter may be omitted by the JS caller: it either has a
|
|
148
280
|
/// default value (Swift applies it) or is an optional type (an absent slot becomes `nil`). The arity
|
|
149
281
|
/// range and the per-arity call branches are derived from this.
|
|
@@ -158,26 +290,23 @@ internal func hasDefaultValue(_ parameter: FunctionParameterSyntax) -> Bool {
|
|
|
158
290
|
return parameter.defaultValue != nil
|
|
159
291
|
}
|
|
160
292
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
JS thread
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
they stay callable from any thread.
|
|
167
|
-
*/
|
|
293
|
+
/// True if the declaration is a `@Event(sync: true)` property. A sync event dispatches inline on the
|
|
294
|
+
/// JS thread instead of scheduling, so `@ExpoModule`/`@SharedObject` stamp it with `@JavaScriptActor`,
|
|
295
|
+
/// making "must be called on the JS thread" a compile-time guarantee at the call site. Async events
|
|
296
|
+
/// (the default) are deliberately not stamped: their `emit` schedules onto the JS thread itself, so
|
|
297
|
+
/// they stay callable from any thread.
|
|
168
298
|
internal func isSyncEventMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
169
299
|
guard let varDecl = decl.as(VariableDeclSyntax.self),
|
|
170
|
-
let attribute = varDecl.attributes.firstAttribute(named: "Event")
|
|
300
|
+
let attribute = varDecl.attributes.firstAttribute(named: "Event")
|
|
301
|
+
else {
|
|
171
302
|
return false
|
|
172
303
|
}
|
|
173
304
|
return boolArgument(of: attribute, label: "sync") == true
|
|
174
305
|
}
|
|
175
306
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
180
|
-
*/
|
|
307
|
+
/// Reads a labeled array-literal argument of an attribute, e.g. `@ExpoModule(classes: [Foo.self, Bar.self])`,
|
|
308
|
+
/// and returns the type names referenced (e.g. `["Foo", "Bar"]`). Each element must be a
|
|
309
|
+
/// `<TypeName>.self` member-access expression; non-conforming elements are skipped silently.
|
|
181
310
|
internal func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
182
311
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
183
312
|
return []
|
|
@@ -189,7 +318,8 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
189
318
|
return array.elements.compactMap { element -> String? in
|
|
190
319
|
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
191
320
|
memberAccess.declName.baseName.text == "self",
|
|
192
|
-
let base = memberAccess.base
|
|
321
|
+
let base = memberAccess.base
|
|
322
|
+
else {
|
|
193
323
|
return nil
|
|
194
324
|
}
|
|
195
325
|
// A bare `CardView.self` has a `DeclReferenceExprSyntax` base; a qualified
|
|
@@ -205,11 +335,9 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
205
335
|
return []
|
|
206
336
|
}
|
|
207
337
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`Module<Foo>` all match an entry of "Module".
|
|
212
|
-
*/
|
|
338
|
+
/// Returns true if the class's inheritance clause names any of the given identifiers.
|
|
339
|
+
/// Matches by base identifier only, so `Module`, `ExpoModulesCore.Module`, and
|
|
340
|
+
/// `Module<Foo>` all match an entry of "Module".
|
|
213
341
|
internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>) -> Bool {
|
|
214
342
|
guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
|
|
215
343
|
return false
|
|
@@ -222,10 +350,8 @@ internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>)
|
|
|
222
350
|
return false
|
|
223
351
|
}
|
|
224
352
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
228
|
-
*/
|
|
353
|
+
/// Returns the rightmost identifier of a type, e.g. `Foo` for `Foo`, `Foo` for
|
|
354
|
+
/// `Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
229
355
|
internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
230
356
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
231
357
|
return identifier.name.text
|
|
@@ -236,10 +362,8 @@ internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
|
236
362
|
return nil
|
|
237
363
|
}
|
|
238
364
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
returns false for any other decl kind.
|
|
242
|
-
*/
|
|
365
|
+
/// True if the declaration carries a `@JS` attribute. Works for functions, properties, and inits;
|
|
366
|
+
/// returns false for any other decl kind.
|
|
243
367
|
internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
244
368
|
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
245
369
|
return funcDecl.attributes.firstAttribute(named: "JS") != nil
|
|
@@ -256,6 +380,7 @@ internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
256
380
|
/// Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
|
|
257
381
|
/// The macro defers to the user when they've already chosen an isolation:
|
|
258
382
|
/// - the member opted out with `@JS(.concurrent)`
|
|
383
|
+
/// - the member lists `@JS(.withAsync("name"))`, whose variant runs it on the concurrent pool
|
|
259
384
|
/// - the `nonisolated` modifier is present on the member
|
|
260
385
|
/// - any attribute whose name matches a known global actor (`@MainActor`, `@JavaScriptActor`)
|
|
261
386
|
/// or follows the `*Actor` naming convention is present on the member or its enclosing type
|
|
@@ -265,7 +390,7 @@ internal func shouldStampJavaScriptActor(
|
|
|
265
390
|
on member: DeclSyntaxProtocol,
|
|
266
391
|
enclosedBy enclosing: some DeclGroupSyntax
|
|
267
392
|
) -> Bool {
|
|
268
|
-
if isConcurrentJSMember(member) {
|
|
393
|
+
if isConcurrentJSMember(member) || hasAsyncVariant(member) {
|
|
269
394
|
return false
|
|
270
395
|
}
|
|
271
396
|
|
|
@@ -312,7 +437,7 @@ internal func memberAttributes(of decl: DeclSyntaxProtocol) -> AttributeListSynt
|
|
|
312
437
|
return AttributeListSyntax()
|
|
313
438
|
}
|
|
314
439
|
|
|
315
|
-
|
|
440
|
+
internal func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool {
|
|
316
441
|
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
317
442
|
return false
|
|
318
443
|
}
|
|
@@ -345,7 +470,8 @@ extension AttributeListSyntax {
|
|
|
345
470
|
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
346
471
|
for element in self {
|
|
347
472
|
if let attr = element.as(AttributeSyntax.self),
|
|
348
|
-
attr.attributeName.trimmedDescription == name
|
|
473
|
+
attr.attributeName.trimmedDescription == name
|
|
474
|
+
{
|
|
349
475
|
return attr
|
|
350
476
|
}
|
|
351
477
|
}
|
|
@@ -425,6 +551,7 @@ internal func collectProperties(
|
|
|
425
551
|
let jsNameOverride = jsNameArgument(of: attribute)
|
|
426
552
|
// A `let` is never settable; only `var` bindings can carry a setter.
|
|
427
553
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
554
|
+
let isLazy = isLazy(varDecl.modifiers)
|
|
428
555
|
|
|
429
556
|
return varDecl.bindings.compactMap { binding in
|
|
430
557
|
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
@@ -433,7 +560,8 @@ internal func collectProperties(
|
|
|
433
560
|
let swiftName = ident.identifier.text
|
|
434
561
|
// Prefer the explicit annotation; recover the type from a literal default (`var x = false`)
|
|
435
562
|
// when there's none. `nil` falls back to inference at the use site.
|
|
436
|
-
let valueType =
|
|
563
|
+
let valueType =
|
|
564
|
+
binding.typeAnnotation?.type.trimmedDescription
|
|
437
565
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
438
566
|
return JSProperty(
|
|
439
567
|
swiftName: swiftName,
|
|
@@ -441,7 +569,8 @@ internal func collectProperties(
|
|
|
441
569
|
valueType: valueType,
|
|
442
570
|
// A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
|
|
443
571
|
freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
|
|
444
|
-
isSettable: isVar && bindingIsSettable(binding),
|
|
572
|
+
isSettable: isVar && !isLazy && bindingIsSettable(binding),
|
|
573
|
+
isLazy: isLazy,
|
|
445
574
|
nameToken: ident.identifier
|
|
446
575
|
)
|
|
447
576
|
}
|