@expo/expo-modules-macros-plugin 0.0.9 → 0.1.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/publish.yml +116 -0
- package/.github/workflows/swift.yml +59 -0
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +11 -4
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +157 -8
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +116 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +424 -135
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +17 -20
- package/apple/build.js +129 -12
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
release_type:
|
|
7
|
+
description: Release type
|
|
8
|
+
type: choice
|
|
9
|
+
required: true
|
|
10
|
+
default: patch
|
|
11
|
+
options:
|
|
12
|
+
- patch
|
|
13
|
+
- minor
|
|
14
|
+
- major
|
|
15
|
+
- prepatch
|
|
16
|
+
- preminor
|
|
17
|
+
- premajor
|
|
18
|
+
- prerelease
|
|
19
|
+
|
|
20
|
+
concurrency:
|
|
21
|
+
group: publish-${{ github.workflow }}
|
|
22
|
+
cancel-in-progress: false
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
publish:
|
|
26
|
+
name: Build and publish to npm
|
|
27
|
+
# Builds the native macOS tool binary, so it must run on macOS.
|
|
28
|
+
# The package declares swift-tools-version 6.2, which requires Xcode 26+.
|
|
29
|
+
runs-on: macos-26
|
|
30
|
+
permissions:
|
|
31
|
+
# Required for npm trusted publishing (OIDC) and provenance.
|
|
32
|
+
id-token: write
|
|
33
|
+
# Required to push the version bump commit/tag and create the release.
|
|
34
|
+
contents: write
|
|
35
|
+
steps:
|
|
36
|
+
- name: Checkout
|
|
37
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
38
|
+
with:
|
|
39
|
+
# The default token lets gh create the release and push the bump
|
|
40
|
+
# commit/tag. Note: commits pushed with GITHUB_TOKEN do NOT trigger
|
|
41
|
+
# other workflows (e.g. the Swift CI), by GitHub's design.
|
|
42
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
|
|
44
|
+
- name: Setup Node
|
|
45
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: 22
|
|
48
|
+
registry-url: https://registry.npmjs.org
|
|
49
|
+
|
|
50
|
+
- name: Update npm
|
|
51
|
+
# Trusted publishing requires npm >= 11.5.1.
|
|
52
|
+
run: npm install -g npm@latest
|
|
53
|
+
|
|
54
|
+
- name: Configure git
|
|
55
|
+
run: |
|
|
56
|
+
git config user.name "expo-bot"
|
|
57
|
+
git config user.email "expo-bot@users.noreply.github.com"
|
|
58
|
+
|
|
59
|
+
- name: Bump version
|
|
60
|
+
id: bump
|
|
61
|
+
# Only edit package.json here; the commit, tag and release are created
|
|
62
|
+
# at the end so a failed build/publish leaves the branch untouched.
|
|
63
|
+
run: |
|
|
64
|
+
version=$(npm version "${{ inputs.release_type }}" --no-git-tag-version)
|
|
65
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
66
|
+
echo "Bumping to $version"
|
|
67
|
+
|
|
68
|
+
- name: Select Xcode
|
|
69
|
+
# Pin to an Xcode that ships Swift 6.2 (required by Package.swift).
|
|
70
|
+
run: sudo xcode-select -switch /Applications/Xcode_26.4.1.app
|
|
71
|
+
|
|
72
|
+
- name: Cache SwiftPM build
|
|
73
|
+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
|
74
|
+
with:
|
|
75
|
+
path: apple/.build
|
|
76
|
+
key: spm-${{ runner.os }}-${{ hashFiles('apple/Package.resolved') }}
|
|
77
|
+
restore-keys: |
|
|
78
|
+
spm-${{ runner.os }}-
|
|
79
|
+
|
|
80
|
+
- name: Resolve dependencies
|
|
81
|
+
run: swift package resolve
|
|
82
|
+
working-directory: apple
|
|
83
|
+
|
|
84
|
+
- name: Build release tool
|
|
85
|
+
# Runs apple/build.js: builds the release plugin binary,
|
|
86
|
+
# copies it to apple/ExpoModulesMacros-tool, and strips it.
|
|
87
|
+
run: npm run build
|
|
88
|
+
|
|
89
|
+
- name: Publish
|
|
90
|
+
# Authentication is handled via OIDC trusted publishing — no token needed.
|
|
91
|
+
# npm generates provenance automatically when publishing via OIDC.
|
|
92
|
+
run: npm publish --access public
|
|
93
|
+
|
|
94
|
+
# Everything below only runs once the publish above has succeeded.
|
|
95
|
+
|
|
96
|
+
- name: Commit, tag and push
|
|
97
|
+
run: |
|
|
98
|
+
version="${{ steps.bump.outputs.version }}"
|
|
99
|
+
git commit -am "Release $version"
|
|
100
|
+
git tag "$version"
|
|
101
|
+
git push --follow-tags origin HEAD
|
|
102
|
+
|
|
103
|
+
- name: Create GitHub release
|
|
104
|
+
env:
|
|
105
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
106
|
+
# Tag prerelease versions (those containing a hyphen) as prereleases.
|
|
107
|
+
run: |
|
|
108
|
+
version="${{ steps.bump.outputs.version }}"
|
|
109
|
+
prerelease=""
|
|
110
|
+
case "$version" in
|
|
111
|
+
*-*) prerelease="--prerelease" ;;
|
|
112
|
+
esac
|
|
113
|
+
gh release create "$version" \
|
|
114
|
+
--title "$version" \
|
|
115
|
+
--generate-notes \
|
|
116
|
+
$prerelease
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Swift
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch: {}
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
defaults:
|
|
15
|
+
run:
|
|
16
|
+
working-directory: apple
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build-and-test:
|
|
20
|
+
name: Build & test
|
|
21
|
+
# The package declares swift-tools-version 6.2, which requires Xcode 26+.
|
|
22
|
+
runs-on: macos-26
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout
|
|
25
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
26
|
+
|
|
27
|
+
- name: Select Xcode
|
|
28
|
+
# Pin to an Xcode that ships Swift 6.2 (required by Package.swift).
|
|
29
|
+
run: sudo xcode-select -switch /Applications/Xcode_26.4.1.app
|
|
30
|
+
|
|
31
|
+
- name: Cache SwiftPM build
|
|
32
|
+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
|
33
|
+
with:
|
|
34
|
+
path: apple/.build
|
|
35
|
+
key: spm-${{ runner.os }}-${{ hashFiles('apple/Package.resolved') }}
|
|
36
|
+
restore-keys: |
|
|
37
|
+
spm-${{ runner.os }}-
|
|
38
|
+
|
|
39
|
+
- name: Resolve dependencies
|
|
40
|
+
run: swift package resolve
|
|
41
|
+
|
|
42
|
+
- name: Build release tool
|
|
43
|
+
# Runs `npm run build` (apple/build.js): builds the universal
|
|
44
|
+
# (arm64 + x86_64) release plugin binary, copies it to
|
|
45
|
+
# apple/ExpoModulesMacros-tool, and strips it.
|
|
46
|
+
working-directory: ${{ github.workspace }}
|
|
47
|
+
run: npm run build
|
|
48
|
+
|
|
49
|
+
- name: Verify universal binary
|
|
50
|
+
run: |
|
|
51
|
+
archs="$(lipo -archs ExpoModulesMacros-tool)"
|
|
52
|
+
echo "Architectures: $archs"
|
|
53
|
+
case "$archs" in *arm64*) ;; *) echo "Missing arm64 slice"; exit 1;; esac
|
|
54
|
+
case "$archs" in *x86_64*) ;; *) echo "Missing x86_64 slice"; exit 1;; esac
|
|
55
|
+
arch -arm64 ./ExpoModulesMacros-tool < /dev/null
|
|
56
|
+
arch -x86_64 ./ExpoModulesMacros-tool < /dev/null
|
|
57
|
+
|
|
58
|
+
- name: Test
|
|
59
|
+
run: swift test -v
|
|
Binary file
|
package/apple/Package.swift
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
3
3
|
|
|
4
4
|
import CompilerPluginSupport
|
|
5
|
+
import Foundation
|
|
5
6
|
import PackageDescription
|
|
6
7
|
|
|
7
8
|
let package = Package(
|
|
@@ -18,8 +19,14 @@ let package = Package(
|
|
|
18
19
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
|
19
20
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
|
20
21
|
]
|
|
21
|
-
)
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
)
|
|
22
25
|
|
|
26
|
+
// The Tests directory is excluded from the published npm package,
|
|
27
|
+
// so only declare the test target when building from the repository.
|
|
28
|
+
if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
|
|
29
|
+
package.targets.append(
|
|
23
30
|
.testTarget(
|
|
24
31
|
name: "ExpoModulesMacrosTests",
|
|
25
32
|
dependencies: [
|
|
@@ -27,6 +34,6 @@ let package = Package(
|
|
|
27
34
|
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
|
|
28
35
|
.product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"),
|
|
29
36
|
]
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -3,13 +3,23 @@ import SwiftSyntaxBuilder
|
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
Macro applied to a module class. It plays three roles, each implemented in its own
|
|
7
|
+
extension below:
|
|
8
|
+
|
|
9
|
+
- `MemberMacro`: scans the class body for `@JS`-marked declarations and synthesizes a
|
|
10
|
+
framework-internal `_synthesizedDefinition()` returning `[AnyDefinition]`, which
|
|
11
|
+
`expo-modules-core` calls automatically and merges into the module's definition. When
|
|
12
|
+
the class doesn't already inherit `Module`/`BaseModule`, it also synthesizes the
|
|
13
|
+
`appContext` storage and `init(appContext:)` those base classes would have provided.
|
|
14
|
+
- `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
|
|
15
|
+
`@ModuleDefinitionBuilder` on a `definition()` method.
|
|
16
|
+
- `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
|
|
17
|
+
|
|
18
|
+
Inheriting from `Module` is therefore optional — a class can carry any superclass (or
|
|
19
|
+
none) and still be a module, because everything inheritance used to provide is synthesized.
|
|
10
20
|
|
|
11
21
|
@ExpoModule
|
|
12
|
-
public final class MyModule
|
|
22
|
+
public final class MyModule {
|
|
13
23
|
public func definition() -> ModuleDefinition {
|
|
14
24
|
Name("MyModule")
|
|
15
25
|
}
|
|
@@ -33,7 +43,7 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
33
43
|
var entries: [String] = ["Name(\"\(moduleName)\")"]
|
|
34
44
|
|
|
35
45
|
for typeName in classListArgument(of: node, label: "classes") {
|
|
36
|
-
entries.append("\(typeName).
|
|
46
|
+
entries.append("\(typeName)._synthesizedClassDefinition()")
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
for member in classDecl.memberBlock.members {
|
|
@@ -54,14 +64,153 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
54
64
|
let lines = entries.map { " \($0)" }.joined(separator: ",\n")
|
|
55
65
|
let body = " return [\n\(lines)\n ]"
|
|
56
66
|
|
|
67
|
+
var emitted: [DeclSyntax] = []
|
|
68
|
+
|
|
69
|
+
// `Module`/`BaseModule` already provide `appContext` storage and the
|
|
70
|
+
// `init(appContext:)` requirement, so we only synthesize them for classes that
|
|
71
|
+
// inherit from neither. Each is skipped individually if the user wrote their own,
|
|
72
|
+
// to avoid emitting a duplicate declaration.
|
|
73
|
+
if !inheritsFromAny(classDecl, names: ["Module", "BaseModule"]) {
|
|
74
|
+
if !hasStoredProperty(classDecl, named: "appContext") {
|
|
75
|
+
emitted.append("public weak var appContext: AppContext?")
|
|
76
|
+
}
|
|
77
|
+
if !hasAppContextInitializer(classDecl) {
|
|
78
|
+
emitted.append(
|
|
79
|
+
"""
|
|
80
|
+
public required init(appContext: AppContext) {
|
|
81
|
+
self.appContext = appContext
|
|
82
|
+
}
|
|
83
|
+
"""
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Always emitted: the definition is derived from the class name and `@JS` members,
|
|
89
|
+
// independent of any `definition()` the user wrote.
|
|
57
90
|
let method: DeclSyntax = """
|
|
58
|
-
public func
|
|
91
|
+
public func _synthesizedDefinition() -> [AnyDefinition] {
|
|
59
92
|
\(raw: body)
|
|
60
93
|
}
|
|
61
94
|
"""
|
|
95
|
+
emitted.append(method)
|
|
96
|
+
|
|
97
|
+
return emitted
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
extension ExpoModuleMacro: MemberAttributeMacro {
|
|
102
|
+
public static func expansion(
|
|
103
|
+
of node: AttributeSyntax,
|
|
104
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
105
|
+
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
106
|
+
in context: some MacroExpansionContext
|
|
107
|
+
) throws -> [AttributeSyntax] {
|
|
108
|
+
// This macro is invoked once per member and may attach more than one attribute,
|
|
109
|
+
// so we collect into an array rather than returning early. The two checks below are
|
|
110
|
+
// independent — a given member matches at most one in practice, but they're written
|
|
111
|
+
// so neither precludes the other.
|
|
112
|
+
var attributes: [AttributeSyntax] = []
|
|
113
|
+
|
|
114
|
+
// `@JS` sync members run on the JS thread; stamp `@JavaScriptActor` so isolation is
|
|
115
|
+
// checked at compile time. Skipped when the member already chose an isolation
|
|
116
|
+
// (`async`, `nonisolated`, or another global actor) — see `shouldStampJavaScriptActor`.
|
|
117
|
+
if memberHasJSAttribute(member),
|
|
118
|
+
shouldStampJavaScriptActor(on: member, enclosedBy: declaration) {
|
|
119
|
+
attributes.append("@JavaScriptActor")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Apply the result builder to `definition()` so the user doesn't have to. Skipped if
|
|
123
|
+
// they already wrote `@ModuleDefinitionBuilder` themselves, which would otherwise be
|
|
124
|
+
// a duplicate attribute.
|
|
125
|
+
if isModuleDefinitionFunction(member),
|
|
126
|
+
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") }) {
|
|
127
|
+
attributes.append("@ModuleDefinitionBuilder")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return attributes
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func isModuleDefinitionFunction(_ member: some DeclSyntaxProtocol) -> Bool {
|
|
135
|
+
guard let funcDecl = member.as(FunctionDeclSyntax.self),
|
|
136
|
+
funcDecl.name.text == "definition",
|
|
137
|
+
funcDecl.signature.parameterClause.parameters.isEmpty,
|
|
138
|
+
let returnType = funcDecl.signature.returnClause?.type else {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
return baseIdentifier(of: returnType) == "ModuleDefinition"
|
|
142
|
+
}
|
|
62
143
|
|
|
63
|
-
|
|
144
|
+
private func hasAttributeName(_ element: AttributeListSyntax.Element, _ name: String) -> Bool {
|
|
145
|
+
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
return attribute.attributeName.trimmedDescription == name
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
extension ExpoModuleMacro: ExtensionMacro {
|
|
152
|
+
public static func expansion(
|
|
153
|
+
of node: AttributeSyntax,
|
|
154
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
155
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
156
|
+
conformingTo protocols: [TypeSyntax],
|
|
157
|
+
in context: some MacroExpansionContext
|
|
158
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
159
|
+
// `protocols` is empty when the compiler already sees the conformance (e.g. it's
|
|
160
|
+
// spelled in the declaration), in which case there's nothing for us to add.
|
|
161
|
+
guard !protocols.isEmpty else {
|
|
162
|
+
return []
|
|
163
|
+
}
|
|
164
|
+
// These base types already supply `AnyModule`; emitting a second conformance here
|
|
165
|
+
// would be redundant and error.
|
|
166
|
+
if let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
167
|
+
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"]) {
|
|
168
|
+
return []
|
|
169
|
+
}
|
|
170
|
+
let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
|
|
171
|
+
return [conformance.cast(ExtensionDeclSyntax.self)]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// MARK: - Synthesized-member detection
|
|
176
|
+
|
|
177
|
+
private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String) -> Bool {
|
|
178
|
+
for member in classDecl.memberBlock.members {
|
|
179
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
for binding in varDecl.bindings {
|
|
183
|
+
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
184
|
+
identifier.identifier.text == name {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
True if the class declares an initializer that satisfies the `init(appContext:)` protocol
|
|
194
|
+
requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
|
|
195
|
+
(or `….AppContext`). Both must match — the label is part of the requirement, so
|
|
196
|
+
`init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
|
|
197
|
+
against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
|
|
198
|
+
syntactic match on the spelled name.
|
|
199
|
+
*/
|
|
200
|
+
private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
201
|
+
for member in classDecl.memberBlock.members {
|
|
202
|
+
guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
let params = initDecl.signature.parameterClause.parameters
|
|
206
|
+
guard params.count == 1, let param = params.first else {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if param.firstName.text == "appContext", baseIdentifier(of: param.type) == "AppContext" {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
64
212
|
}
|
|
213
|
+
return false
|
|
65
214
|
}
|
|
66
215
|
|
|
67
216
|
// MARK: - Member builders
|
|
@@ -39,6 +39,122 @@ internal func classListArgument(of attribute: AttributeSyntax, label: String) ->
|
|
|
39
39
|
return []
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
Returns true if the class's inheritance clause names any of the given identifiers.
|
|
44
|
+
Matches by base identifier only, so `Module`, `ExpoModulesCore.Module`, and
|
|
45
|
+
`Module<Foo>` all match an entry of "Module".
|
|
46
|
+
*/
|
|
47
|
+
internal func inheritsFromAny(_ classDecl: ClassDeclSyntax, names: Set<String>) -> Bool {
|
|
48
|
+
guard let inherited = classDecl.inheritanceClause?.inheritedTypes else {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
for entry in inherited {
|
|
52
|
+
if let name = baseIdentifier(of: entry.type), names.contains(name) {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Returns the rightmost identifier of a type, e.g. `Foo` for `Foo`, `Foo` for
|
|
61
|
+
`Module.Foo`, and nil for composed or generic shapes the macro doesn't need to handle.
|
|
62
|
+
*/
|
|
63
|
+
internal func baseIdentifier(of type: TypeSyntax) -> String? {
|
|
64
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
65
|
+
return identifier.name.text
|
|
66
|
+
}
|
|
67
|
+
if let member = type.as(MemberTypeSyntax.self) {
|
|
68
|
+
return member.name.text
|
|
69
|
+
}
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
True if the declaration carries a `@JS` attribute. Works for functions, properties, and inits;
|
|
75
|
+
returns false for any other decl kind.
|
|
76
|
+
*/
|
|
77
|
+
internal func memberHasJSAttribute(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
78
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
79
|
+
return funcDecl.attributes.firstAttribute(named: "JS") != nil
|
|
80
|
+
}
|
|
81
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
82
|
+
return varDecl.attributes.firstAttribute(named: "JS") != nil
|
|
83
|
+
}
|
|
84
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
85
|
+
return initDecl.attributes.firstAttribute(named: "JS") != nil
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
Decides whether the macro should stamp `@JavaScriptActor` on a `@JS`-marked member.
|
|
92
|
+
The macro defers to the user when they've already chosen an isolation:
|
|
93
|
+
- the `nonisolated` modifier is present on the member
|
|
94
|
+
- any attribute whose name matches a known global actor (`@MainActor`, `@JavaScriptActor`)
|
|
95
|
+
or follows the `*Actor` naming convention is present on the member or its enclosing type
|
|
96
|
+
Async members never get the stamp because `AsyncFunction` controls their dispatch separately.
|
|
97
|
+
*/
|
|
98
|
+
internal func shouldStampJavaScriptActor(
|
|
99
|
+
on member: DeclSyntaxProtocol,
|
|
100
|
+
enclosedBy enclosing: some DeclGroupSyntax
|
|
101
|
+
) -> Bool {
|
|
102
|
+
let modifiers = memberModifiers(of: member)
|
|
103
|
+
if modifiers.contains(where: { $0.name.text == "nonisolated" }) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if let funcDecl = member.as(FunctionDeclSyntax.self),
|
|
108
|
+
funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let memberAttributes = memberAttributes(of: member)
|
|
113
|
+
if memberAttributes.contains(where: hasGlobalActorShape) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if enclosing.attributes.contains(where: hasGlobalActorShape) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func memberModifiers(of decl: DeclSyntaxProtocol) -> DeclModifierListSyntax {
|
|
125
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
126
|
+
return funcDecl.modifiers
|
|
127
|
+
}
|
|
128
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
129
|
+
return varDecl.modifiers
|
|
130
|
+
}
|
|
131
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
132
|
+
return initDecl.modifiers
|
|
133
|
+
}
|
|
134
|
+
return DeclModifierListSyntax()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
internal func memberAttributes(of decl: DeclSyntaxProtocol) -> AttributeListSyntax {
|
|
138
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self) {
|
|
139
|
+
return funcDecl.attributes
|
|
140
|
+
}
|
|
141
|
+
if let varDecl = decl.as(VariableDeclSyntax.self) {
|
|
142
|
+
return varDecl.attributes
|
|
143
|
+
}
|
|
144
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self) {
|
|
145
|
+
return initDecl.attributes
|
|
146
|
+
}
|
|
147
|
+
return AttributeListSyntax()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func hasGlobalActorShape(_ element: AttributeListSyntax.Element) -> Bool {
|
|
151
|
+
guard let attribute = element.as(AttributeSyntax.self) else {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
let name = attribute.attributeName.trimmedDescription
|
|
155
|
+
return name.hasSuffix("Actor")
|
|
156
|
+
}
|
|
157
|
+
|
|
42
158
|
extension AttributeListSyntax {
|
|
43
159
|
internal func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
44
160
|
for element in self {
|