-
-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement @ObservableDefault macro #189
Open
kevinrpb
wants to merge
11
commits into
sindresorhus:main
Choose a base branch
from
kevinrpb:macros
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
4827461
Implement @ObservableDefaults macro
kevinrpb 21c4281
Fix test names, add tests for @Observable result
kevinrpb 85587a0
Add docstring to ObservableDefaults macro
kevinrpb 028cdfb
Fix incorrect filename for macros plugin, fix lint
kevinrpb 70f569e
Rename `@ObservableDefaults` macro to `@Default`
kevinrpb 6cc37e0
Fix lint warnings
kevinrpb 147892d
Update readme to include `@Default` macro
kevinrpb 8708fd5
Update readme.md
sindresorhus 1c9262c
Update Default.swift
sindresorhus 017a42d
Rename @Default macro back to @ObservableDefault
kevinrpb 242aba5
Add build time disclaimer in @ObservableDefault section of readme
kevinrpb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"originHash" : "1ba60ebd54db82e47e39bc8db179589187c069067eb0a8cd6ec19d2301c5abc4", | ||
"pins" : [ | ||
{ | ||
"identity" : "swift-syntax", | ||
"kind" : "remoteSourceControl", | ||
"location" : "https://github.com/swiftlang/swift-syntax", | ||
"state" : { | ||
"revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", | ||
"version" : "600.0.0" | ||
} | ||
} | ||
], | ||
"version" : 3 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import Defaults | ||
import Foundation | ||
|
||
/** | ||
Attached macro that adds support for using ``Defaults`` in ``@Observable`` classes. | ||
|
||
- Important: To prevent issues with ``@Observable``, you need to also add ``@ObservationIgnored`` to the attached property. | ||
|
||
This macro adds accessor blocks to the attached property similar to those added by `@Observable`. | ||
|
||
For example, given the following source: | ||
|
||
```swift | ||
@Observable | ||
final class CatModel { | ||
@ObservableDefault(.cat) | ||
@ObservationIgnored | ||
var catName: String | ||
} | ||
``` | ||
|
||
The macro will generate the following expansion: | ||
|
||
```swift | ||
@Observable | ||
final class CatModel { | ||
@ObservationIgnored | ||
var catName: String { | ||
get { | ||
access(keypath: \.catName) | ||
return Defaults[.cat] | ||
} | ||
set { | ||
withMutation(keyPath: \catName) { | ||
Defaults[.cat] = newValue | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
*/ | ||
@attached(accessor, names: named(get), named(set)) | ||
public macro ObservableDefault<Value>(_ key: Defaults.Key<Value>) = | ||
#externalMacro( | ||
module: "DefaultsMacrosDeclarations", | ||
type: "ObservableDefaultMacro" | ||
) |
9 changes: 9 additions & 0 deletions
9
Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntaxMacros | ||
|
||
@main | ||
struct DefaultsMacrosPlugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
ObservableDefaultMacro.self | ||
] | ||
} |
117 changes: 117 additions & 0 deletions
117
Sources/DefaultsMacrosDeclarations/ObservableDefaultMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftDiagnostics | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct ObservableDefaultMacro: AccessorMacro { | ||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingAccessorsOf declaration: some DeclSyntaxProtocol, | ||
in context: some MacroExpansionContext | ||
) throws -> [AccessorDeclSyntax] { | ||
// Must be attached to a property declaration. | ||
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else { | ||
throw ObservableDefaultMacroError.notAttachedToProperty | ||
} | ||
|
||
// Must be attached to a variable property (i.e. `var` and not `let`). | ||
guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else { | ||
throw ObservableDefaultMacroError.notAttachedToVariable | ||
} | ||
|
||
// Must be attached to a single property. | ||
guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else { | ||
throw ObservableDefaultMacroError.notAttachedToSingleProperty | ||
} | ||
|
||
// Must not provide an initializer for the property (i.e. not assign a value). | ||
guard binding.initializer == nil else { | ||
throw ObservableDefaultMacroError.attachedToPropertyWithInitializer | ||
} | ||
|
||
// Must not be attached to property with existing accessor block. | ||
guard binding.accessorBlock == nil else { | ||
throw ObservableDefaultMacroError.attachedToPropertyWithAccessorBlock | ||
} | ||
|
||
// Must use Identifier Pattern. | ||
// See https://swiftinit.org/docs/swift-syntax/swiftsyntax/identifierpatternsyntax | ||
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { | ||
throw ObservableDefaultMacroError.attachedToPropertyWithoutIdentifierProperty | ||
} | ||
|
||
// Must receive arguments | ||
guard let arguments = node.arguments else { | ||
throw ObservableDefaultMacroError.calledWithoutArguments | ||
} | ||
|
||
// Must be called with Labeled Expression. | ||
// See https://swiftinit.org/docs/swift-syntax/swiftsyntax/labeledexprlistsyntax | ||
guard let expressionList = arguments.as(LabeledExprListSyntax.self) else { | ||
throw ObservableDefaultMacroError.calledWithoutLabeledExpression | ||
} | ||
|
||
// Must only receive one argument. | ||
guard expressionList.count == 1, let expression = expressionList.first?.expression else { | ||
throw ObservableDefaultMacroError.calledWithMultipleArguments | ||
} | ||
|
||
return [ | ||
#""" | ||
get { | ||
access(keyPath: \.\#(pattern)) | ||
return Defaults[\#(expression)] | ||
} | ||
"""#, | ||
#""" | ||
set { | ||
withMutation(keyPath: \.\#(pattern)) { | ||
Defaults[\#(expression)] = newValue | ||
} | ||
} | ||
"""# | ||
] | ||
} | ||
} | ||
|
||
enum ObservableDefaultMacroError: Error { | ||
case notAttachedToProperty | ||
case notAttachedToVariable | ||
case notAttachedToSingleProperty | ||
case attachedToPropertyWithInitializer | ||
case attachedToPropertyWithAccessorBlock | ||
case attachedToPropertyWithoutIdentifierProperty | ||
case calledWithoutArguments | ||
case calledWithoutLabeledExpression | ||
case calledWithMultipleArguments | ||
case calledWithoutFunctionSyntax | ||
case calledWithoutKeyArgument | ||
case calledWithUnsupportedExpression | ||
} | ||
|
||
extension ObservableDefaultMacroError: CustomStringConvertible { | ||
var description: String { | ||
switch self { | ||
case .notAttachedToProperty: | ||
"@ObservableDefault must be attached to a property." | ||
case .notAttachedToVariable: | ||
"@ObservableDefault must be attached to a `var` property." | ||
case .notAttachedToSingleProperty: | ||
"@ObservableDefault can only be attached to a single property." | ||
case .attachedToPropertyWithInitializer: | ||
"@ObservableDefault must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition." | ||
case .attachedToPropertyWithAccessorBlock: | ||
"@ObservableDefault must not be attached to a property with accessor block." | ||
case .attachedToPropertyWithoutIdentifierProperty: | ||
"@ObservableDefault could not identify the attached property." | ||
case .calledWithoutArguments, | ||
.calledWithoutLabeledExpression, | ||
.calledWithMultipleArguments, | ||
.calledWithoutFunctionSyntax, | ||
.calledWithoutKeyArgument, | ||
.calledWithUnsupportedExpression: | ||
"@ObservableDefault must be called with (1) argument of type `Defaults.Key`" | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then you can remove
ObservableDefaultMacroError
from the throw statements.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! typed throws are here and I forgot about them. Neat.