expo-modules-macros 0.31.0 → 0.33.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/{swift.yml → macos.yml} +12 -3
- package/AGENTS.md +4 -2
- package/Package.swift +10 -1
- package/README.md +6 -6
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +124 -5
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/InheritedMembers.swift +2 -1
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ResolveRefs.swift +76 -17
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ScanExports.swift +15 -8
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +174 -20
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +14 -6
- package/apple/Sources/ExpoModulesCLI/Core/Detection.swift +6 -0
- package/apple/Sources/ExpoModulesCLI/Core/DetectionVisitor.swift +41 -2
- package/apple/Sources/ExpoModulesCLI/Core/SourceScan.swift +3 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +12 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +12 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoViewMacro.swift +64 -30
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +124 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/RecordMacro.swift +68 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +79 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +574 -23
- package/apple/Sources/ExpoModulesMacrosCore/ProvidedMacros.swift +1 -1
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +28 -2
- package/apple/Sources/ExpoModulesMacrosPlugin/ExpoModulesMacrosPlugin.swift +1 -0
- package/build/types.d.ts +58 -3
- package/package.json +5 -5
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`,
|
|
4
|
-
/// their `@JS` members
|
|
5
|
-
/// Recognition is purely syntactic and re-reads
|
|
6
|
-
/// imported), so it stays in step with `JSFunction` /
|
|
7
|
-
/// `RecordProperty`.
|
|
3
|
+
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
4
|
+
/// `@ExpoView`, and `@ViewProps` type: their `@JS` members, record properties, view props, and view
|
|
5
|
+
/// events. The deep counterpart to `DetectionVisitor`. Recognition is purely syntactic and re-reads
|
|
6
|
+
/// what the macros read (the macro target can't be imported), so it stays in step with `JSFunction` /
|
|
7
|
+
/// `JSProperty` / `JSConstructor` / `RecordProperty` / `validatedViewProps`.
|
|
8
8
|
///
|
|
9
9
|
/// Types nested in another type's body, or in a file-scope `extension`, are reported under their
|
|
10
10
|
/// qualified Swift name (`Options.Encoding`), so a ref to one can be resolved from the scope it is
|
|
@@ -12,10 +12,10 @@ import SwiftSyntax
|
|
|
12
12
|
///
|
|
13
13
|
/// Each declaration and member reports the platforms that build it (see `PlatformResolver`), and one
|
|
14
14
|
/// that no platform builds is not reported. `#if` is read only where the runtime sees what it holds:
|
|
15
|
-
/// around types, `@Event` members, and enum cases. `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
16
|
-
/// `@Union` read only the members written directly in the type body, so `@JS`
|
|
17
|
-
/// properties,
|
|
18
|
-
/// expands on its own declaration, and the compiler reads enum cases itself.
|
|
15
|
+
/// around types, `@Event` members, and enum cases. `@ExpoModule`, `@SharedObject`, `@Record`,
|
|
16
|
+
/// `@Union`, and `@ViewProps` read only the members written directly in the type body, so `@JS`
|
|
17
|
+
/// members, record properties, union cases, and view props in a `#if` clause are never bound and are
|
|
18
|
+
/// not reported. An `@Event` expands on its own declaration, and the compiler reads enum cases itself.
|
|
19
19
|
final class SurfaceVisitor: SyntaxVisitor {
|
|
20
20
|
private let file: String
|
|
21
21
|
private let platformResolver: PlatformResolver
|
|
@@ -24,6 +24,10 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
24
24
|
private(set) var records: [ExportedRecord] = []
|
|
25
25
|
private(set) var enums: [ExportedEnum] = []
|
|
26
26
|
private(set) var unions: [ExportedUnion] = []
|
|
27
|
+
private(set) var views: [ExportedView] = []
|
|
28
|
+
/// The `@ViewProps` structs, which `ExportedSurface.joiningViewProps(_:)` copies into the views once
|
|
29
|
+
/// every file has been walked.
|
|
30
|
+
private(set) var viewProps: [ViewPropsDeclaration] = []
|
|
27
31
|
|
|
28
32
|
init(file: String, tree: SourceFileSyntax) {
|
|
29
33
|
self.file = file
|
|
@@ -120,8 +124,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/// Routes a class or struct to the right collector based on which Expo macro it carries. A type
|
|
123
|
-
/// carrying none of them is ignored.
|
|
124
|
-
///
|
|
127
|
+
/// carrying none of them is ignored. The macros are mutually exclusive in practice, so the first
|
|
128
|
+
/// match wins.
|
|
125
129
|
///
|
|
126
130
|
/// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
|
|
127
131
|
/// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
|
|
@@ -201,6 +205,38 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
201
205
|
file: file
|
|
202
206
|
)
|
|
203
207
|
)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// `@ExpoView` applies only to a class, and needs the props type as its generic argument.
|
|
212
|
+
if node.is(ClassDeclSyntax.self),
|
|
213
|
+
let attribute = attributes.firstAttribute(named: DetectedMacro.expoView.rawValue),
|
|
214
|
+
let propsType = genericArgumentType(of: attribute)
|
|
215
|
+
{
|
|
216
|
+
// Only a file-scope view is reported, as `scan-modules` reports only those.
|
|
217
|
+
guard scope.isEmpty else {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
// `props` and `events` are filled in by `ExportedSurface.joiningViewProps(_:)`, since the
|
|
221
|
+
// `@ViewProps` struct can be declared in another file.
|
|
222
|
+
views.append(
|
|
223
|
+
ExportedView(
|
|
224
|
+
name: qualified,
|
|
225
|
+
jsName: name,
|
|
226
|
+
propsType: String(propsType.trimmedDescription.filter { $0 != "`" }),
|
|
227
|
+
propsTypeLookupName: spellingWithoutGenericArguments(of: propsType),
|
|
228
|
+
accessLevel: accessLevel,
|
|
229
|
+
platforms: platforms,
|
|
230
|
+
file: file
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `@ViewProps` applies only to a struct.
|
|
237
|
+
if node.is(StructDeclSyntax.self), attributes.firstAttribute(named: DetectedMacro.viewProps.rawValue) != nil {
|
|
238
|
+
let (props, events) = collectViewProps(members)
|
|
239
|
+
viewProps.append(ViewPropsDeclaration(name: qualified, props: props, events: events, platforms: platforms))
|
|
204
240
|
}
|
|
205
241
|
}
|
|
206
242
|
|
|
@@ -425,7 +461,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
425
461
|
// `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
|
|
426
462
|
for member in members {
|
|
427
463
|
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
428
|
-
!
|
|
464
|
+
!isExcludedByModifier(varDecl.modifiers)
|
|
429
465
|
else {
|
|
430
466
|
continue
|
|
431
467
|
}
|
|
@@ -459,6 +495,76 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
459
495
|
return properties
|
|
460
496
|
}
|
|
461
497
|
|
|
498
|
+
/// The value props and events of a `@ViewProps` struct, mirroring `validatedViewProps` in the macro:
|
|
499
|
+
/// every binding of a `var` or `let` that no modifier excludes, except a computed one. A property
|
|
500
|
+
/// with `willSet`/`didSet` or a `set` counts, as in the macro. A function-typed property is an event,
|
|
501
|
+
/// and every other one a value prop.
|
|
502
|
+
///
|
|
503
|
+
/// The scan stays lenient where the macro reports an error, and leaves out only what can't be
|
|
504
|
+
/// described: a property whose type can't be determined, an optional function type, and an event
|
|
505
|
+
/// that doesn't return `Void`. A value prop the macro rejects for another reason, such as a `let` or
|
|
506
|
+
/// a missing default, is still reported.
|
|
507
|
+
private func collectViewProps(
|
|
508
|
+
_ members: MemberBlockItemListSyntax
|
|
509
|
+
) -> (props: [ExportedViewProp], events: [ExportedViewEvent]) {
|
|
510
|
+
var props: [ExportedViewProp] = []
|
|
511
|
+
var events: [ExportedViewEvent] = []
|
|
512
|
+
|
|
513
|
+
// `@ViewProps` reads only the properties written directly in the body, so `#if` is not looked into.
|
|
514
|
+
for member in members {
|
|
515
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
516
|
+
!isExcludedByModifier(varDecl.modifiers)
|
|
517
|
+
else {
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
520
|
+
let platforms = platformResolver.platforms(of: varDecl)
|
|
521
|
+
guard !platforms.isEmpty else {
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
for binding in varDecl.bindings {
|
|
525
|
+
if binding.accessorBlock != nil && !isSettable(binding: binding, isLet: false) {
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
let name = ident.identifier.identifierName
|
|
532
|
+
let annotation = binding.typeAnnotation?.type
|
|
533
|
+
|
|
534
|
+
if let annotation, let functionType = viewEventFunctionType(of: annotation) {
|
|
535
|
+
// An optional function type and a non-`Void` result are errors in the macro.
|
|
536
|
+
guard !isOptionalEventType(annotation), isVoidEventReturn(functionType.returnClause.type) else {
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
let parameters = functionType.parameters
|
|
540
|
+
events.append(
|
|
541
|
+
ExportedViewEvent(
|
|
542
|
+
name: name,
|
|
543
|
+
payload: parameters.count == 1 ? parameters.first.map { typeNode(from: $0.type) } : nil,
|
|
544
|
+
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
545
|
+
platforms: platforms
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
guard let type = valueTypeNode(annotation: annotation, initializer: binding.initializer?.value) else {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
props.append(
|
|
555
|
+
ExportedViewProp(
|
|
556
|
+
name: name,
|
|
557
|
+
type: type,
|
|
558
|
+
defaultValue: binding.initializer?.value.trimmedDescription,
|
|
559
|
+
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
560
|
+
platforms: platforms
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return (props, events)
|
|
566
|
+
}
|
|
567
|
+
|
|
462
568
|
/// The declared cases of an enum, in source order. One `case` declaration can introduce several
|
|
463
569
|
/// cases (`case a, b`), so each element is read separately. A case carrying associated values is
|
|
464
570
|
/// skipped: it has no raw value, so it can't cross the boundary as one.
|
|
@@ -706,6 +812,44 @@ private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
|
706
812
|
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
707
813
|
}
|
|
708
814
|
|
|
815
|
+
/// The function type a `@ViewProps` property declares, or `nil` when the type isn't a function.
|
|
816
|
+
/// Unwraps parentheses, attributes (`@Sendable`), and the optional spellings `T?`, `T!`, and
|
|
817
|
+
/// `Optional<T>`, like `underlyingFunctionType` in `ViewPropsMacro`.
|
|
818
|
+
private func viewEventFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax? {
|
|
819
|
+
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
820
|
+
return functionType
|
|
821
|
+
}
|
|
822
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first {
|
|
823
|
+
return viewEventFunctionType(of: only.type)
|
|
824
|
+
}
|
|
825
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
826
|
+
return viewEventFunctionType(of: attributed.baseType)
|
|
827
|
+
}
|
|
828
|
+
if let optional = type.as(OptionalTypeSyntax.self) {
|
|
829
|
+
return viewEventFunctionType(of: optional.wrappedType)
|
|
830
|
+
}
|
|
831
|
+
if let implicitlyUnwrapped = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
832
|
+
return viewEventFunctionType(of: implicitlyUnwrapped.wrappedType)
|
|
833
|
+
}
|
|
834
|
+
if let identifier = type.as(IdentifierTypeSyntax.self),
|
|
835
|
+
identifier.name.text == "Optional",
|
|
836
|
+
let argument = identifier.genericArgumentClause?.arguments.first,
|
|
837
|
+
case .type(let wrapped) = argument.argument
|
|
838
|
+
{
|
|
839
|
+
return viewEventFunctionType(of: wrapped)
|
|
840
|
+
}
|
|
841
|
+
return nil
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// True when a `@ViewProps` event is written as an optional (`(() -> Void)?`, also after attributes),
|
|
845
|
+
/// which the macro rejects. Like the macros' `isOptionalType`, it reads only the outermost type.
|
|
846
|
+
private func isOptionalEventType(_ type: TypeSyntax) -> Bool {
|
|
847
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
848
|
+
return isOptionalEventType(attributed.baseType)
|
|
849
|
+
}
|
|
850
|
+
return isOptionalType(type)
|
|
851
|
+
}
|
|
852
|
+
|
|
709
853
|
/// True if an `@Event` attribute lists the given option member (`@Event(.inline)` or
|
|
710
854
|
/// `@Event("name", EventOptions.inline)`). Like `hasOption` in the macros, but without the array-literal
|
|
711
855
|
/// branch, which `EventOptions...` doesn't accept.
|
|
@@ -836,9 +980,10 @@ private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
|
836
980
|
}
|
|
837
981
|
}
|
|
838
982
|
|
|
839
|
-
/// True when a modifier excludes a property from being a `@Record` field
|
|
840
|
-
/// `private`, `fileprivate`, `lazy`), mirroring `
|
|
841
|
-
|
|
983
|
+
/// True when a modifier excludes a property from being a `@Record` field or a `@ViewProps` prop
|
|
984
|
+
/// (`static`, `class`, `private`, `fileprivate`, `lazy`), mirroring `isExcludedByModifier` in the
|
|
985
|
+
/// macros.
|
|
986
|
+
private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
842
987
|
modifiers.contains { modifier in
|
|
843
988
|
switch modifier.name.tokenKind {
|
|
844
989
|
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
@@ -951,9 +1096,17 @@ func rawValueType(of clause: InheritanceClauseSyntax?) -> TypeNode? {
|
|
|
951
1096
|
return typeNode(from: first)
|
|
952
1097
|
}
|
|
953
1098
|
|
|
954
|
-
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`,
|
|
955
|
-
/// `isOptionalType`.
|
|
1099
|
+
/// True when a type is written as an optional: `T?`, `T!`, or `Optional<T>`, also in parentheses
|
|
1100
|
+
/// (`(T?)`) or behind attributes, mirroring the macros' `isOptionalType`.
|
|
956
1101
|
private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
1102
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
1103
|
+
only.firstName == nil
|
|
1104
|
+
{
|
|
1105
|
+
return isOptionalType(only.type)
|
|
1106
|
+
}
|
|
1107
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
1108
|
+
return isOptionalType(attributed.baseType)
|
|
1109
|
+
}
|
|
957
1110
|
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
958
1111
|
return true
|
|
959
1112
|
}
|
|
@@ -963,13 +1116,14 @@ private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
963
1116
|
return false
|
|
964
1117
|
}
|
|
965
1118
|
|
|
966
|
-
/// The first attribute whose
|
|
967
|
-
///
|
|
1119
|
+
/// The first attribute whose name matches `name` as `attributeName(of:)` reads it, so
|
|
1120
|
+
/// `@ExpoView<CardProps>` and `@ExpoModulesCore.ExpoView<CardProps>` match `ExpoView`. A local copy of
|
|
1121
|
+
/// the macros' helper (the macro target can't be imported here).
|
|
968
1122
|
extension AttributeListSyntax {
|
|
969
1123
|
fileprivate func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
970
1124
|
for element in self {
|
|
971
1125
|
if let attribute = element.as(AttributeSyntax.self),
|
|
972
|
-
|
|
1126
|
+
attributeName(of: attribute) == name
|
|
973
1127
|
{
|
|
974
1128
|
return attribute
|
|
975
1129
|
}
|
|
@@ -34,8 +34,8 @@ public enum Scanner {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`,
|
|
38
|
-
/// class in `sharedObjects`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
37
|
+
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, a `@SharedObject`
|
|
38
|
+
/// class in `sharedObjects`, or an `@ExpoView` class in `views`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
39
39
|
/// the Swift class name, the JS name it registers under, the platforms that include it, and the file
|
|
40
40
|
/// it's in. The richer fields the visitor captures (declaration kind, raw macro arguments,
|
|
41
41
|
/// line/column) are dropped here, because this command doesn't need them (the macro is always on a
|
|
@@ -45,7 +45,7 @@ struct ScannedClass: Codable, Equatable {
|
|
|
45
45
|
let name: String
|
|
46
46
|
|
|
47
47
|
/// The fully-resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override when
|
|
48
|
-
/// present, otherwise the class name. Resolved here (rather than left `nil`) so it matches how the
|
|
48
|
+
/// present, otherwise the class name. `@ExpoView` takes no name, so a view's JS name is its class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
49
|
/// macro derives the name and the consumer never has to apply the fallback itself.
|
|
50
50
|
let jsName: String
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ struct ScannedClass: Codable, Equatable {
|
|
|
70
70
|
/// `ScannedClass`, so `expo-modules-autolinking` can verify it understands the output before
|
|
71
71
|
/// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
|
|
72
72
|
/// per-module `platforms` list and made the module list platform-agnostic. A new field that older
|
|
73
|
-
/// consumers can ignore, such as `sharedObjects`, doesn't change the version.
|
|
73
|
+
/// consumers can ignore, such as `sharedObjects` or `views`, doesn't change the version.
|
|
74
74
|
let scanModulesSchemaVersion = 2
|
|
75
75
|
|
|
76
76
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
@@ -84,6 +84,10 @@ struct ScanModulesResult: Codable, Equatable {
|
|
|
84
84
|
/// `modules`.
|
|
85
85
|
let sharedObjects: [ScannedClass]
|
|
86
86
|
|
|
87
|
+
/// The top-level `@ExpoView` classes, in the same order and with the same platform rules as
|
|
88
|
+
/// `modules`.
|
|
89
|
+
let views: [ScannedClass]
|
|
90
|
+
|
|
87
91
|
/// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
|
|
88
92
|
/// in the report rather than on stderr so the consumer can attach them to its own output.
|
|
89
93
|
let warnings: [ScanWarning]
|
|
@@ -102,7 +106,7 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
102
106
|
(condition: "visionOS", reported: "visionOS"),
|
|
103
107
|
]
|
|
104
108
|
|
|
105
|
-
/// Scans the given paths for top-level `@ExpoModule` and `@
|
|
109
|
+
/// Scans the given paths for top-level `@ExpoModule`, `@SharedObject` and `@ExpoView` types and returns every one
|
|
106
110
|
/// found in any
|
|
107
111
|
/// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
|
|
108
112
|
/// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
|
|
@@ -115,10 +119,11 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
115
119
|
func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
|
|
116
120
|
var modules: [ScannedClass] = []
|
|
117
121
|
var sharedObjects: [ScannedClass] = []
|
|
122
|
+
var views: [ScannedClass] = []
|
|
118
123
|
var warnings: [ScanWarning] = []
|
|
119
124
|
var seenWarnings = Set<ScanWarning>()
|
|
120
125
|
|
|
121
|
-
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject]
|
|
126
|
+
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject, .expoView]
|
|
122
127
|
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
123
128
|
let tree = Parser.parse(source: source)
|
|
124
129
|
|
|
@@ -162,6 +167,8 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
162
167
|
switch detection.macro {
|
|
163
168
|
case .sharedObject:
|
|
164
169
|
sharedObjects.append(scanned)
|
|
170
|
+
case .expoView:
|
|
171
|
+
views.append(scanned)
|
|
165
172
|
default:
|
|
166
173
|
modules.append(scanned)
|
|
167
174
|
}
|
|
@@ -172,6 +179,7 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
172
179
|
schemaVersion: scanModulesSchemaVersion,
|
|
173
180
|
modules: modules,
|
|
174
181
|
sharedObjects: sharedObjects,
|
|
182
|
+
views: views,
|
|
175
183
|
warnings: warnings,
|
|
176
184
|
stats: stats
|
|
177
185
|
)
|
|
@@ -7,6 +7,8 @@ enum DetectedMacro: String, Codable, CaseIterable {
|
|
|
7
7
|
case sharedObject = "SharedObject"
|
|
8
8
|
case record = "Record"
|
|
9
9
|
case union = "Union"
|
|
10
|
+
case expoView = "ExpoView"
|
|
11
|
+
case viewProps = "ViewProps"
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/// A single argument passed to a macro, e.g. `"Foo"` or `classes: [Bar.self]`. The label is `nil`
|
|
@@ -43,6 +45,10 @@ struct Detection: Codable, Equatable {
|
|
|
43
45
|
/// `@SharedObject("Baz")`, otherwise `nil` (the name defaults to `name` at expansion time).
|
|
44
46
|
let jsName: String?
|
|
45
47
|
|
|
48
|
+
/// The generic argument of the attribute as written, which is the props type of `@ExpoView<CardProps>`,
|
|
49
|
+
/// otherwise `nil`.
|
|
50
|
+
var genericArgument: String? = nil
|
|
51
|
+
|
|
46
52
|
/// Every argument passed to the macro, in source order, e.g. `@ExpoModule("Foo", classes: [Bar.self])`
|
|
47
53
|
/// yields a positional `"Foo"` and a `classes:` argument. Empty when the macro is written bare.
|
|
48
54
|
let arguments: [MacroArgument]
|
|
@@ -3,7 +3,7 @@ import SwiftIfConfig
|
|
|
3
3
|
import SwiftSyntax
|
|
4
4
|
|
|
5
5
|
/// Walks a parsed source file and records top-level declarations carrying `@ExpoModule`, `@JS`,
|
|
6
|
-
/// `@SharedObject`, or `@
|
|
6
|
+
/// `@SharedObject`, `@Record`, `@Union`, `@ExpoView`, or `@ViewProps`. Only file-scope declarations are considered: these macros apply to
|
|
7
7
|
/// top-level types, so descending into type and function bodies would only surface false positives.
|
|
8
8
|
/// Recognition mirrors the macros themselves — a purely syntactic match on the spelled attribute
|
|
9
9
|
/// name — so it sees the same declarations the compiler would hand the plugin, without compiling
|
|
@@ -119,7 +119,7 @@ final class DetectionVisitor: SyntaxVisitor {
|
|
|
119
119
|
) {
|
|
120
120
|
for element in attributes {
|
|
121
121
|
guard let attribute = element.as(AttributeSyntax.self),
|
|
122
|
-
let macro = DetectedMacro(rawValue: attribute
|
|
122
|
+
let macro = DetectedMacro(rawValue: attributeName(of: attribute)),
|
|
123
123
|
detectedMacros.contains(macro)
|
|
124
124
|
else {
|
|
125
125
|
continue
|
|
@@ -132,6 +132,7 @@ final class DetectionVisitor: SyntaxVisitor {
|
|
|
132
132
|
declarationKind: kind,
|
|
133
133
|
accessLevel: accessLevel(of: modifiers),
|
|
134
134
|
jsName: stringArgument(of: attribute),
|
|
135
|
+
genericArgument: genericArgument(of: attribute),
|
|
135
136
|
arguments: arguments(of: attribute),
|
|
136
137
|
file: file,
|
|
137
138
|
line: location.line,
|
|
@@ -155,6 +156,44 @@ private func accessLevel(of modifiers: DeclModifierListSyntax) -> String {
|
|
|
155
156
|
return "internal"
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/// The attribute's name without a generic argument clause, so `@ExpoView<CardProps>` is `ExpoView`. A
|
|
160
|
+
/// name qualified with the module that declares the macros is read without the module, so
|
|
161
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>` is `ExpoView` too.
|
|
162
|
+
func attributeName(of attribute: AttributeSyntax) -> String {
|
|
163
|
+
if let identifier = attribute.attributeName.as(IdentifierTypeSyntax.self) {
|
|
164
|
+
return identifier.name.text
|
|
165
|
+
}
|
|
166
|
+
if let member = attribute.attributeName.as(MemberTypeSyntax.self),
|
|
167
|
+
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == macrosModuleName
|
|
168
|
+
{
|
|
169
|
+
return member.name.text
|
|
170
|
+
}
|
|
171
|
+
return attribute.attributeName.trimmedDescription
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The module that declares the macros, which an attribute may be qualified with:
|
|
175
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>`.
|
|
176
|
+
let macrosModuleName = "ExpoModulesCore"
|
|
177
|
+
|
|
178
|
+
/// The first generic argument of the attribute as written, e.g. `@ExpoView<CardProps>` -> "CardProps".
|
|
179
|
+
/// Returns `nil` when the attribute has no generic argument clause.
|
|
180
|
+
private func genericArgument(of attribute: AttributeSyntax) -> String? {
|
|
181
|
+
return genericArgumentType(of: attribute)?.trimmedDescription
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// The first generic argument of the attribute, `CardProps` in `@ExpoView<CardProps>` and in
|
|
185
|
+
/// `@ExpoModulesCore.ExpoView<CardProps>`, or `nil` when the attribute has no generic argument clause.
|
|
186
|
+
/// Like `genericArgument(of:)` in `ExpoViewMacro`.
|
|
187
|
+
func genericArgumentType(of attribute: AttributeSyntax) -> TypeSyntax? {
|
|
188
|
+
let clause =
|
|
189
|
+
attribute.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause
|
|
190
|
+
?? attribute.attributeName.as(MemberTypeSyntax.self)?.genericArgumentClause
|
|
191
|
+
guard let first = clause?.arguments.first, case .type(let type) = first.argument else {
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
return type
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
/// Every argument passed to the attribute, in source order, each as a label (or `nil` when
|
|
159
198
|
/// positional) plus the value expression's source text. Returns an empty array when the attribute
|
|
160
199
|
/// is written bare (`@ExpoModule`) or with empty parens.
|
|
@@ -106,9 +106,10 @@ struct MacroPrefilter {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/// A pre-filter for attributes that `DetectedMacro` doesn't list, such as every macro that `expand`
|
|
109
|
-
/// runs.
|
|
109
|
+
/// runs. Each name also matches qualified with the module that declares the macros
|
|
110
|
+
/// (`@ExpoModulesCore.ExpoView`), since the scanners read that spelling as the bare one.
|
|
110
111
|
init(attributeNames: Set<String>, conformances: Set<String> = []) {
|
|
111
|
-
attributes = attributeNames.
|
|
112
|
+
attributes = attributeNames.flatMap { [Array($0.utf8), Array("\(macrosModuleName).\($0)".utf8)] }
|
|
112
113
|
bareNames = conformances.map { Array($0.utf8) }
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -29,8 +29,8 @@ import SwiftSyntaxMacros
|
|
|
29
29
|
/// The synthesized property is deliberately **not** isolated to `@JavaScriptActor`, unlike `@JS`
|
|
30
30
|
/// members: `emit` is itself non-isolated and schedules the dispatch onto the JS thread internally,
|
|
31
31
|
/// so the event is callable from any thread or isolation with no actor hop at the call site. It is
|
|
32
|
-
/// also self-contained: `@ExpoModule`/`@SharedObject`
|
|
33
|
-
///
|
|
32
|
+
/// also self-contained: `@ExpoModule`/`@SharedObject` don't register `@Event` members anywhere. They
|
|
33
|
+
/// read the event names only to warn when two events share one (see `diagnoseEventNameCollisions`).
|
|
34
34
|
///
|
|
35
35
|
/// Two options change how the event is dispatched, and each one calls its own `EventEmitter` method,
|
|
36
36
|
/// which is callable from any thread as well:
|
|
@@ -293,6 +293,16 @@ private func isDispatchOption(_ expression: ExprSyntax) -> Bool {
|
|
|
293
293
|
return name == "inline" || name == "blocking"
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/// The JS name of an `@Event` property: the `@Event("…")` override, or the default name derived from
|
|
297
|
+
/// the property name. `nil` when the declaration isn't a single named property, which the macro reports.
|
|
298
|
+
internal func eventJSName(of varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> String? {
|
|
299
|
+
guard varDecl.bindings.count == 1, let identifier = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)
|
|
300
|
+
else {
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
return jsNameArgument(of: attribute) ?? defaultEventName(for: withoutBackticks(identifier.identifier.text))
|
|
304
|
+
}
|
|
305
|
+
|
|
296
306
|
// MARK: - Default event name
|
|
297
307
|
|
|
298
308
|
/// The default JS event name for a property: the Swift name with the conventional `on` prefix
|
|
@@ -63,6 +63,16 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
63
63
|
for member in classDecl.memberBlock.members {
|
|
64
64
|
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
65
65
|
let decl = aliases.resolving(member.decl)
|
|
66
|
+
// A generic member is reported by `@JS` and left out, so the build has no errors in its binding.
|
|
67
|
+
if memberHasJSAttribute(decl) && jsGenericParameterClause(of: decl) != nil {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
// So is a `static` or `class` member: the module object binds only instance members.
|
|
71
|
+
if memberHasJSAttribute(decl),
|
|
72
|
+
let modifiers = decl.asProtocol(WithModifiersSyntax.self)?.modifiers, isTypeLevel(modifiers)
|
|
73
|
+
{
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
66
76
|
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
67
77
|
assertions.append(assertion)
|
|
68
78
|
}
|
|
@@ -86,6 +96,8 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
diagnoseJSNameCollisions(functions: functions, properties: properties, in: context)
|
|
99
|
+
diagnoseReservedJSNames(functions: functions, properties: properties, on: .module, in: context)
|
|
100
|
+
diagnoseEventNameCollisions(in: classDecl.memberBlock, in: context)
|
|
89
101
|
|
|
90
102
|
var emitted: [DeclSyntax] = []
|
|
91
103
|
|