Skip to content

Commit

Permalink
Upgrade to Swift Syntax for Swift 6 (#74)
Browse files Browse the repository at this point in the history
# Upgrade to Swift Syntax for Swift 6

## ♻️ Current situation & Problem
This PR upgrades to the upcoming release of SwiftSyntax. This allows us
to provide additional functionality like verifying the `lexicalContext`
of an account details property. This way, we can check that the
`@AccountKey` macro can only be applied inside an extension to
`AccountDetails`.


## ⚙️ Release Notes 
* Improve diagnostics around the `@AccountKey` macro.

## 📚 Documentation
--

## ✅ Testing
Added additional unit tests to verify the updated diagnostics.

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Oct 17, 2024
1 parent 397a55a commit 7ef29a7
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 100 deletions.
21 changes: 0 additions & 21 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,6 @@ jobs:
destination: 'platform=macOS,arch=arm64'
artifactname: SpeziAccount-macOS.xcresult
resultBundle: SpeziAccount-macOS.xcresult
buildandtest_ios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziAccount
xcodeversion: latest
swiftVersion: 6
artifactname: SpeziAccount-Latest.xcresult
resultBundle: SpeziAccount-Latest.xcresult
buildandtest_visionos:
name: Build and Test Swift Package visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand All @@ -61,17 +51,6 @@ jobs:
scheme: TestApp
artifactname: TestApp-iOS.xcresult
resultBundle: TestApp-iOS.xcresult
buildandtestuitests_ios_latest:
name: Build and Test UI Tests Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
scheme: TestApp
xcodeversion: latest
swiftVersion: 6
artifactname: TestApp-iOS-Latest.xcresult
resultBundle: TestApp-iOS-Latest.xcresult
buildandtestuitests_visionos:
name: Build and Test UI Tests visionOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down
39 changes: 10 additions & 29 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

//
// This source file is part of the Spezi open source project
Expand All @@ -13,13 +13,6 @@ import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


let package = Package(
name: "SpeziAccount",
defaultLocalization: "en",
Expand All @@ -32,15 +25,15 @@ let package = Package(
.library(name: "SpeziAccount", targets: ["SpeziAccount"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "2.0.0-beta.2"),
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.7.3"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.6.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage", from: "1.2.0"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.1.1"),
.package(url: "https://github.com/apple/swift-collections", from: "1.1.2"),
.package(url: "https://github.com/apple/swift-atomics", from: "1.2.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", from: "510.0.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0")
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.0.0-beta.2"),
.package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.7.3"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.6.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.2.0"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions.git", from: "1.1.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.2"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-prerelease-2024-08-14"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.17.0")
] + swiftLintPackage(),
targets: [
.macro(
Expand All @@ -50,9 +43,6 @@ let package = Package(
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftDiagnostics", package: "swift-syntax")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
Expand All @@ -73,9 +63,6 @@ let package = Package(
resources: [
.process("Resources")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
Expand All @@ -87,9 +74,6 @@ let package = Package(
.product(name: "XCTSpezi", package: "Spezi"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
Expand All @@ -99,9 +83,6 @@ let package = Package(
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI


public struct AccountServiceButton<Label: View>: View {
private let action: () async throws -> Void
private let action: @MainActor () async throws -> Void
private let label: Label

@Binding private var state: ViewState
Expand All @@ -31,7 +31,7 @@ public struct AccountServiceButton<Label: View>: View {
public init(
_ titleKey: LocalizedStringResource,
systemImage: String = "person.crop.square",
action: @escaping () async -> Void
action: @escaping @MainActor () async -> Void
) where Label == SwiftUI.Label<Text, Image> {
self.init(titleKey, systemImage: systemImage, state: .constant(.idle), action: action)
}
Expand All @@ -40,7 +40,7 @@ public struct AccountServiceButton<Label: View>: View {
_ titleKey: LocalizedStringResource,
systemImage: String = "person.crop.square",
state: Binding<ViewState>,
action: @escaping () async throws -> Void
action: @escaping @MainActor () async throws -> Void
) where Label == SwiftUI.Label<Text, Image> {
self.init(state: state, action: action) {
SwiftUI.Label {
Expand All @@ -54,7 +54,7 @@ public struct AccountServiceButton<Label: View>: View {
public init(
_ titleKey: LocalizedStringResource,
image: ImageResource,
action: @escaping () async -> Void
action: @escaping @MainActor () async -> Void
) where Label == SwiftUI.Label<Text, Image> {
self.init(titleKey, image: image, state: .constant(.idle), action: action)
}
Expand All @@ -63,7 +63,7 @@ public struct AccountServiceButton<Label: View>: View {
_ titleKey: LocalizedStringResource,
image: ImageResource,
state: Binding<ViewState>,
action: @escaping () async throws -> Void
action: @escaping @MainActor () async throws -> Void
) where Label == SwiftUI.Label<Text, Image> {
self.init(state: state, action: action) {
SwiftUI.Label {
Expand All @@ -75,15 +75,15 @@ public struct AccountServiceButton<Label: View>: View {
}

public init(
action: @escaping () async -> Void,
action: @escaping @MainActor () async -> Void,
@ViewBuilder label: () -> Label
) {
self.init(state: .constant(.idle), action: action, label: label)
}

public init(
state: Binding<ViewState>,
action: @escaping () async throws -> Void,
action: @escaping @MainActor () async throws -> Void,
@ViewBuilder label: () -> Label
) {
self.action = action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,20 @@ struct SignupSetupView<Credential: Sendable>: View {

#if DEBUG
#Preview {
@State var style: PresentedSetupStyle<UserIdPasswordCredential> = .signup
@State var presentingSignup = false
@Previewable @State var style: PresentedSetupStyle<UserIdPasswordCredential> = .signup
@Previewable @State var presentingSignup = false

return SignupSetupView(style: $style, login: { _ in }, presentingSignup: $presentingSignup)
SignupSetupView(style: $style, login: { _ in }, presentingSignup: $presentingSignup)
.previewWith {
AccountConfiguration(service: InMemoryAccountService())
}
}

#Preview {
@State var style: PresentedSetupStyle<UserIdPasswordCredential> = .signup
@State var presentingSignup = false
@Previewable @State var style: PresentedSetupStyle<UserIdPasswordCredential> = .signup
@Previewable @State var presentingSignup = false

return SignupSetupView(style: $style, login: nil, presentingSignup: $presentingSignup)
SignupSetupView(style: $style, login: nil, presentingSignup: $presentingSignup)
.previewWith {
AccountConfiguration(service: InMemoryAccountService())
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/SpeziAccount/Views/DataEntry/BoolEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ extension AccountKey where Value == Bool {

#if DEBUG
#Preview {
@State var value = false
return Form {
@Previewable @State var value = false
Form {
BoolEntryView<MockBoolKey>($value)
}
}

#Preview {
@State var value = true
return Form {
@Previewable @State var value = true
Form {
BoolEntryView<MockBoolKey>($value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,19 @@ extension AccountKey where Value: PickerValue, Value.AllCases: RandomAccessColle

#if DEBUG
#Preview {
@State var genderIdentity: GenderIdentity = .male
@Previewable @State var genderIdentity: GenderIdentity = .male

return Form {
Form {
Grid {
CaseIterablePickerEntryView(\.genderIdentity, $genderIdentity)
}
}
}

#Preview {
@State var genderIdentity: GenderIdentity = .male
@Previewable @State var genderIdentity: GenderIdentity = .male

return Grid {
Grid {
CaseIterablePickerEntryView(\.genderIdentity, $genderIdentity)
}
.padding(32)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ extension AccountKey where Value: FixedWidthInteger {

#if DEBUG
#Preview {
@State var value = 3
return List {
@Previewable @State var value = 3
List {
FixedWidthIntegerEntryView<MockNumericKey>($value)
}
.previewWith {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ extension AccountKey where Value: BinaryFloatingPoint {

#if DEBUG
#Preview {
@State var value = 3.15
return List {
@Previewable @State var value = 3.15
List {
FloatingPointEntryView<MockDoubleKey>($value)
}
.previewWith {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziAccount/Views/DataEntry/StringEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ extension AccountKey where Value == String {

#if DEBUG
#Preview {
@State var value = "Hello World"
return List {
@Previewable @State var value = "Hello World"
List {
StringEntryView(\.userId, $value)
.validate(input: value, rules: .nonEmpty)
}
Expand Down
48 changes: 41 additions & 7 deletions Sources/SpeziAccountMacros/AccountKeyMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension AccountKeyMacro: AccessorMacro {
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self),
let binding = variableDeclaration.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticsError(syntax: declaration, message: "'@AccountKey' was unable to determine the property name", id: .invalidSyntax)
return [] // diagnostic is provided by the peer macro expansion
}

let getAccessor: AccessorDeclSyntax =
Expand All @@ -53,14 +53,48 @@ extension AccountKeyMacro: PeerMacro {
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self),
let binding = variableDeclaration.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticsError(syntax: declaration, message: "'@AccountKey' was unable to determine the property name", id: .invalidSyntax)
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else {
throw DiagnosticsError(syntax: declaration, message: "'@AccountKey' can only be applied to a 'var' declaration", id: .invalidSyntax)
}

guard let binding = variableDeclaration.bindings.first,
variableDeclaration.bindings.count == 1 else {
throw DiagnosticsError(
syntax: declaration,
message: "'@AccountKey' can only be applied to a 'var' declaration with a single binding",
id: .invalidSyntax
)
}

guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticsError(
syntax: declaration,
message: "'@AccountKey' can only be applied to a 'var' declaration with a simple name",
id: .invalidSyntax
)
}

#if compiler(>=6)
// with previous compilers the `lexicalContext` is empty
guard let rootContext = context.lexicalContext.first,
let extensionDecl = rootContext.as(ExtensionDeclSyntax.self),
let extendedTypeIdentifier = extensionDecl.extendedType.as(IdentifierTypeSyntax.self),
let extensionIdentifier = extendedTypeIdentifier.name.identifier,
extensionIdentifier.name == "AccountDetails" else {
throw DiagnosticsError(
syntax: declaration,
message: "'@AccountKey' can only be applied to 'var' declarations inside of an extension to 'AccountDetails'",
id: .invalidSyntax
)
}
#endif

guard let typeAnnotation = binding.typeAnnotation else {
throw DiagnosticsError(syntax: binding, message: "Variable binding is missing the type annotation", id: .invalidSyntax)
throw DiagnosticsError(syntax: binding, message: "Variable binding is missing a type annotation", id: .invalidSyntax)
}

if let initializer = binding.initializer {
throw DiagnosticsError(syntax: initializer, message: "Variable binding cannot have a initializer", id: .invalidSyntax)
}

guard case let .argumentList(argumentList) = node.arguments else {
Expand Down Expand Up @@ -108,7 +142,7 @@ extension AccountKeyMacro: PeerMacro {


let modifier: TokenSyntax? = variableDeclaration.modifiers
.compactMap { modifier in
.compactMap { (modifier: DeclModifierSyntax) -> TokenSyntax? in
guard case let .keyword(keyword) = modifier.name.tokenKind else {
return nil
}
Expand Down
Loading

0 comments on commit 7ef29a7

Please sign in to comment.