From 371e6ce4b5ffc7e54148e89245fe6e607909b000 Mon Sep 17 00:00:00 2001 From: hank121314 Date: Fri, 31 Jul 2026 00:09:10 +0800 Subject: [PATCH 1/4] feat: add `SerdeDefault` macro --- .gitignore | 1 + .swiftlint.yml | 6 +- Sources/Defaults/Defaults+Bridge.swift | 17 +- Sources/Defaults/Utilities.swift | 5 +- Sources/DefaultsMacros/SerdeDefault.swift | 61 ++ .../DefaultsMacrosPlugin.swift | 4 +- .../CodeBlock/Deserialize.swift | 119 +++ .../CodeGenerating/CodeBlock/Serialize.swift | 121 +++ .../CodeGenerating/CodeGenerating.swift | 19 + .../Semantics/NominalDecl+Bridge.swift | 119 +++ .../SerdeDefault/Semantics/NominalDecl.swift | 97 +++ .../SerdeDefault/Semantics/Properties.swift | 31 + .../Semantics/Properties/BasicProperty.swift | 85 ++ .../Semantics/Properties/Property.swift | 74 ++ .../Properties/SerializableProperty.swift | 87 ++ .../Semantics/Properties/StrategyFinder.swift | 58 ++ .../Semantics/SerdeAttribute.swift | 100 +++ .../SerdeDefault/SerdeDefault+Error.swift | 121 +++ .../SerdeDefault/Syntax/AccessLevel.swift | 44 + .../Constants/IdentifierTypeSyntax.swift | 78 ++ .../Syntax/Constants/MemberTypeSyntax.swift | 22 + .../Extensions/DeclModifierSyntax.swift | 12 + .../Syntax/Extensions/ExprSyntax.swift | 72 ++ .../Syntax/Extensions/OptionalType.swift | 24 + .../Syntax/Extensions/SyntaxProtocol.swift | 36 + .../Syntax/Extensions/TokenSyntax.swift | 23 + .../Extensions/TypeSyntaxProtocol.swift | 118 +++ .../SerdeDefault/Syntax/FunctionDecl.swift | 89 ++ .../SerdeDefault/Syntax/PatternBinding.swift | 71 ++ .../SerdeDefault/Syntax/VariableDecl.swift | 72 ++ .../SerdeDefaultMacro.swift | 57 ++ .../SerdeMacro.swift | 21 + .../ObservableDefaultMacroTests.swift | 4 +- .../SerdeDefaultMacroTests.swift | 784 ++++++++++++++++++ .../DefaultsAnySeriliazableTests.swift | 2 +- .../DefaultsCustomBridgeTests.swift | 1 - 36 files changed, 2636 insertions(+), 19 deletions(-) create mode 100644 Sources/DefaultsMacros/SerdeDefault.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/AccessLevel.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift create mode 100644 Sources/DefaultsMacrosDeclarations/SerdeMacro.swift create mode 100644 Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift diff --git a/.gitignore b/.gitignore index 0854237e..02576fed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.build +/.swiftlint-cache /Packages xcuserdata project.xcworkspace diff --git a/.swiftlint.yml b/.swiftlint.yml index 4602d5cc..6cc51860 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -87,7 +87,7 @@ only_rules: - number_separator - opening_brace - operator_usage_whitespace - - operator_whitespace + - function_name_whitespace - overridden_super_call - prefer_self_in_static_references - prefer_self_type_over_type_of_self @@ -104,7 +104,7 @@ only_rules: - redundant_discardable_let - redundant_nil_coalescing - redundant_objc_attribute - - redundant_optional_initialization + - implicit_optional_initialization - redundant_set_access_control - redundant_string_enum_value - redundant_type_annotation @@ -153,6 +153,8 @@ only_rules: - xct_specific_matcher - xctfail_message - yoda_condition +excluded: + - .build analyzer_rules: - capture_variable - unused_declaration diff --git a/Sources/Defaults/Defaults+Bridge.swift b/Sources/Defaults/Defaults+Bridge.swift index 7dc27344..83115f87 100644 --- a/Sources/Defaults/Defaults+Bridge.swift +++ b/Sources/Defaults/Defaults+Bridge.swift @@ -323,16 +323,15 @@ extension Defaults { if Bound.isNativelySupportedType { return [value.lowerBound, value.upperBound] - } else { - guard - let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value), - let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value) - else { - return nil - } - - return [lowerBound, upperBound] } + guard + let lowerBound = Bound.bridge.serialize(value.lowerBound as? Bound.Value), + let upperBound = Bound.bridge.serialize(value.upperBound as? Bound.Value) + else { + return nil + } + + return [lowerBound, upperBound] } public func deserialize(_ object: Serializable?) -> Value? { diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index a51d4b13..9ea70049 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -200,7 +200,7 @@ extension Defaults.Serializable { return Value.toValue(anyObject) ``` */ - static func toValue(_ anyObject: Any, type: T.Type = Self.self) -> T? { + public static func toValue(_ anyObject: Any, type: T.Type = Self.self) -> T? { if T.isNativelySupportedType, let anyObject = anyObject as? T @@ -228,8 +228,7 @@ extension Defaults.Serializable { set(Value.toSerialize(value), forKey: key) ``` */ - @usableFromInline - static func toSerializable(_ value: T) -> Any? { + public static func toSerializable(_ value: T) -> Any? { guard !T.isNativelySupportedType else { return value } diff --git a/Sources/DefaultsMacros/SerdeDefault.swift b/Sources/DefaultsMacros/SerdeDefault.swift new file mode 100644 index 00000000..4dd59aff --- /dev/null +++ b/Sources/DefaultsMacros/SerdeDefault.swift @@ -0,0 +1,61 @@ +import Defaults + +/** +Supported arguments for `@serde`. +*/ +public enum SerdeOption { + case skip +} + +/** +Attached macro that synthesizes `Defaults.Serializable` conformance for a struct. + +This macro generates: + +1. A nested `Bridge` type named `Bridge`. +2. `serialize(_:)` and `deserialize(_:)` implementations based on stored properties. +3. A static `bridge` instance used by `Defaults`. + +For example, given: + +```swift +@SerdeDefault +struct User { + var name: String + var age: UInt? +} +``` + +The macro generates an extension equivalent to: + +```swift +extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge { + typealias Value = User + typealias Serializable = [String: Any] + // serialize(_:) and deserialize(_:) + } + + static let bridge = UserBridge() +} +``` + +- Important: In this stage, `@SerdeDefault` supports `struct` declarations only. +*/ +@attached(extension, conformances: Defaults.Serializable, names: named(bridge), suffixed(Bridge)) +public macro SerdeDefault() = #externalMacro(module: "DefaultsMacrosDeclarations", type: "SerdeDefaultMacro") + +/** +Peer macro used on stored properties in `@SerdeDefault` structs to configure serde behavior. + +Supported arguments: + +1. `@serde(.skip)`: Excludes the property from serialization and deserialization. + +- Important: `skip` is only valid on optional properties. +*/ +@attached(peer) +public macro serde(_ options: SerdeOption...) = #externalMacro( + module: "DefaultsMacrosDeclarations", + type: "SerdeMacro" +) diff --git a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift index 689f41dd..2c12ef78 100644 --- a/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift +++ b/Sources/DefaultsMacrosDeclarations/DefaultsMacrosPlugin.swift @@ -4,6 +4,8 @@ import SwiftSyntaxMacros @main struct DefaultsMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - ObservableDefaultMacro.self + ObservableDefaultMacro.self, + SerdeDefaultMacro.self, + SerdeMacro.self ] } diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift new file mode 100644 index 00000000..a9522e3a --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift @@ -0,0 +1,119 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Code generator for func deserialize code blocks. + + For native properties: + + ```swift + let property = base["property"] as? PropertyType + ``` + For serializable properties: + + ```swift + let property = PropertyType.toValue(base["property"] as Any, type: PropertyType.self) + ``` + */ + enum Deserialize: CodeGenerating { + struct Context { + /** + The serialized dictionary parameter identifier being deserialized. + */ + let base: TokenSyntax + /** + The nominal type being rebuilt. + */ + let typeSyntax: TypeSyntax + } + + /** + Generated deserialization code for one property. + + - `omitted`: Property not in the memberwise initializer — excluded entirely. + - `skipped`: Property marked with `@serde(.skip)` — passed as `nil` in the initializer. + - `assignment`: Normal property with a binding statement and an initializer argument. + */ + enum CodeGenerated { + case omitted + case skipped(identifier: TokenSyntax) + case assignment(identifier: TokenSyntax, statement: CodeBlockItemSyntax) + } + + struct DeserializedCodeGenerated { + let codesGenerated: [CodeGenerated] + let typeSyntax: TypeSyntax + + /** + The list of statements for initializing the properties. + */ + var statements: CodeBlockItemListSyntax { + let statements = codesGenerated.compactMap { code -> CodeBlockItemSyntax? in + guard case .assignment(_, let statement) = code else { + return nil + } + return statement + } + return CodeBlockItemListSyntax( + statements.enumerated().map { index, statement in + statement.with(\.leadingTrivia, index == 0 ? [] : .newline) + } + ) + } + + /** + The initializer expression for the nominal type, using the generated arguments. + + + */ + var initializer: FunctionCallExprSyntax { + let callee: ExprSyntax = "\(typeSyntax.trimmed)" + let arguments = codesGenerated.compactMap { code -> (label: TokenSyntax, expression: ExprSyntax)? in + switch code { + case .omitted: + return nil + // Properties marked with `@serde(.skip)` are passed as `nil` in the initializer. + case .skipped(let identifier): + return (identifier, ExprSyntax(NilLiteralExprSyntax())) + case .assignment(let identifier, _): + return (identifier, ExprSyntax(identifier.declReference())) + } + } + let labeled = arguments.enumerated().map { index, argument in + LabeledExprSyntax( + label: argument.label, + colon: .colonToken(), + expression: argument.expression, + trailingComma: index < arguments.count - 1 ? .commaToken() : nil + ) + } + return FunctionCallExprSyntax(callee: callee) { + LabeledExprListSyntax(labeled) + } + } + } + + static func codeGeneratedBlock( + from properties: [any Property], + in context: Context + ) -> CodeBlockSyntax { + let codesGenerated = properties.map { property in + property.deserialize(in: context) + } + let codeGenerated = DeserializedCodeGenerated( + codesGenerated: codesGenerated, + typeSyntax: context.typeSyntax + ) + + return CodeBlockSyntax( + """ + { + guard let \(context.base) else { return nil } + \(codeGenerated.statements) + return \(codeGenerated.initializer) + } + """ + ) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift new file mode 100644 index 00000000..83cfd6b6 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift @@ -0,0 +1,121 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +extension SerdeDefault { + /** + Code generator for func serialize code blocks. + + For native properties: + + ```swift + serialized["property"] = value.property + ``` + + For serializable properties: + + ```swift + guard let property = PropertyType.toSerializable(value.property) else { return nil } + serialized["property"] = property + ``` + */ + enum Serialize: CodeGenerating { + struct Context { + /** + The value parameter identifier being serialized. + */ + let base: TokenSyntax + /** + The dictionary identifier that receives serialized values. + */ + let serialized: TokenSyntax + /** + The expansion context, used to mint collision-free identifiers. + */ + let macroContext: any MacroExpansionContext + + /** + Returns a collision-free name for the temporary that holds `identifier`'s value. + + `base` and `serialized` are the only other identifiers declared in the generated + function, and property names reach it only as member accesses (`value.name`) or + dictionary keys. Minting the temporaries is therefore enough to keep the whole + function hygienic — a property named `serialized` would otherwise shadow the + dictionary it is being written into. + */ + func makeUniqueName(for identifier: TokenSyntax) -> TokenSyntax { + macroContext.makeUniqueName(identifier.text) + } + } + + /** + Generated serialization code for one property. + + - `omitted`: Property excluded from serialization (e.g. marked with `@serde(.skip)`). + - `basic`: Direct dictionary entries for non-optional native values. + - `optional`: Conditional assignments for optional values. + - `serializable`: Conversion code for values backed by `Defaults.Serializable`. + */ + enum CodeGenerated { + case omitted + case basic(DictionaryElementSyntax) + case optional(CodeBlockItemSyntax) + case serializable(CodeBlockItemSyntax) + } + + struct SerializedCodeGenerated { + let codesGenerated: [CodeGenerated] + + var initializer: ExprSyntax { + let initializerElements = codesGenerated.compactMap { code in + if case .basic(let element) = code { + return element + } + return nil + } + if initializerElements.isEmpty { + return ExprSyntax("[:]") + } + return ExprSyntax( + stringLiteral: "[\(initializerElements.map(\.description).joined(separator: ", "))]" + ) + } + + var assignments: CodeBlockItemListSyntax { + let assignments: [CodeBlockItemSyntax] = codesGenerated.compactMap { code in + switch code { + case .omitted, .basic: + return nil + case .optional(let code), .serializable(let code): + return code + } + } + return CodeBlockItemListSyntax(assignments) + } + } + + static func codeGeneratedBlock( + from properties: [any Property], + in context: Context + ) -> CodeBlockSyntax { + let codesGenerated = properties.map { property in + property.serialize(in: context) + } + let codeGenerated = SerializedCodeGenerated(codesGenerated: codesGenerated) + // Only bind with `var` when something actually mutates the dictionary, otherwise + // every expansion warns that it was never mutated. + let bindingSpecifier: TokenSyntax = + codeGenerated.assignments.isEmpty ? .keyword(.let) : .keyword(.var) + + return CodeBlockSyntax( + """ + { + guard let \(context.base) else { return nil } + \(bindingSpecifier) \(context.serialized): \(IdentifierTypeSyntax.Serializable) = \(codeGenerated.initializer) + \(codeGenerated.assignments) + return \(context.serialized) + } + """ + ) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift new file mode 100644 index 00000000..5636c31e --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Protocol for property-code generators. + */ + protocol CodeGenerating { + associatedtype Context + associatedtype CodeGenerated + + /** + Generates a code block for multiple properties from their generated code. + */ + static func codeGeneratedBlock( + from properties: [any Property], + in context: Context + ) -> CodeBlockSyntax + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift new file mode 100644 index 00000000..47392783 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift @@ -0,0 +1,119 @@ +import SwiftSyntax + +extension SerdeDefault.NominalDecl { + /** + The generated bridge struct name. + */ + private var bridgeName: TokenSyntax { + identifier.suffix(IdentifierTypeSyntax.Bridge.name.text) + } + + /** + The access level for generated bridge declarations. + */ + private var bridgeAccessLevel: AccessLevel? { + accessLevel > .internal ? accessLevel : nil + } + + /** + Generates the stored bridge instance for the expanded extension. + + The generated member is `static let bridge = ()`. If the source + nominal type is wider than `internal`, that access level is preserved. + + - Returns: The generated `bridge` declaration. + */ + func bridgeVariableDecl() -> VariableDeclSyntax { + let modifiers = bridgeAccessLevel.map { [$0.declModifier, .static] } ?? [.static] + let binding = PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: IdentifierTypeSyntax.bridge.name), + initializer: InitializerClauseSyntax( + value: FunctionCallExprSyntax(callee: bridgeName.declReference()) + )) + return VariableDeclSyntax( + modifiers: DeclModifierListSyntax(modifiers), + bindingSpecifier: .keyword(.let), + bindings: PatternBindingListSyntax([binding]) + ) + } + + /** + Generates the nested `Defaults.Bridge` implementation. + + - Returns: The generated bridge type declaration. + */ + func bridgeStructDecl() -> StructDeclSyntax { + let modifiers = DeclModifierListSyntax(bridgeAccessLevel.map { [$0.declModifier] } ?? []) + let deserializeContext = self.deserializeContext + let valueTypeAlias = TypeAliasDeclSyntax( + modifiers: modifiers, + name: IdentifierTypeSyntax.Value.name, + initializer: TypeInitializerClauseSyntax(value: typeSyntax) + ) + // TODO: Allow overriding this alias and keep this as the default. + let serializableTypeAlias = TypeAliasDeclSyntax( + modifiers: modifiers, + name: IdentifierTypeSyntax.Serializable.name, + initializer: TypeInitializerClauseSyntax( + value: DictionaryTypeSyntax( + key: IdentifierTypeSyntax.String, + value: IdentifierTypeSyntax.Any, + ) + ) + ) + + // Generate `serialize` function. + let serializeCodeBlock = properties.codeGenerated( + using: SerdeDefault.Serialize.self, + in: serializeContext + ) + let serializeFunction = SerdeDefault.FunctionDecl.Builder( + name: IdentifierTypeSyntax.serialize.name + ) + .setAccessLevel(accessLevel: .public) + .addParameter(secondName: serializeContext.base, type: IdentifierTypeSyntax.Value.optional()) + .setReturnType(returnType: IdentifierTypeSyntax.Serializable.optional()) + .build { _ in serializeCodeBlock } + + // Generate `deserialize` function. + let deserializeCodeBlock = properties.codeGenerated( + using: SerdeDefault.Deserialize.self, + in: deserializeContext + ) + let deserializeFunction = SerdeDefault.FunctionDecl.Builder( + name: IdentifierTypeSyntax.deserialize.name + ) + .setAccessLevel(accessLevel: .public) + .addParameter( + secondName: deserializeContext.base, type: IdentifierTypeSyntax.Serializable.optional() + ) + .setReturnType(returnType: IdentifierTypeSyntax.Value.optional()) + .build { _ in deserializeCodeBlock } + + let memberBlock = MemberBlockSyntax( + members: MemberBlockItemListSyntax([ + MemberBlockItemSyntax(decl: DeclSyntax(valueTypeAlias)), + MemberBlockItemSyntax(decl: DeclSyntax(serializableTypeAlias)), + MemberBlockItemSyntax(decl: DeclSyntax(serializeFunction)), + MemberBlockItemSyntax(decl: DeclSyntax(deserializeFunction)) + ]) + ) + + return StructDeclSyntax( + modifiers: modifiers, + name: bridgeName, + inheritanceClause: InheritanceClauseSyntax( + inheritedTypes: InheritedTypeListSyntax([ + InheritedTypeSyntax( + type: MemberTypeSyntax.DefaultsBridge, + trailingComma: .commaToken() + ), + InheritedTypeSyntax( + type: IdentifierTypeSyntax.Sendable + ) + ]) + ), + memberBlock: memberBlock + ) + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift new file mode 100644 index 00000000..b4627f48 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift @@ -0,0 +1,97 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +extension SerdeDefault { + /** + Nominal declaration annotated with `@SerdeDefault`. + */ + struct NominalDecl { + /** + The nominal type name. + */ + let identifier: TokenSyntax + /** + The access level of the nominal type. + + If the declaration does not specify an access level, this defaults to `internal`. + */ + let accessLevel: AccessLevel + /** + The actual type syntax for the nominal type. + */ + let typeSyntax: TypeSyntax + /** + The properties of the nominal type. + */ + let properties: Properties + /** + The context for code generation of the `serialize` function. + */ + let serializeContext: SerdeDefault.Serialize.Context + /** + The context for code generation of the `deserialize` function. + */ + let deserializeContext: SerdeDefault.Deserialize.Context + + init( + declaration: some SwiftSyntax.DeclGroupSyntax, + type: some TypeSyntaxProtocol, + macroContext: MacroExpansionContext + ) throws(SerdeDefault.Error) { + // TODO: Support class declarations. + guard let structDecl = declaration.as(StructDeclSyntax.self) else { + throw SerdeDefault.Error.unexpectedNominalType(declaration) + } + try self.init(structDecl: structDecl, type: type, context: macroContext) + } + + private init( + structDecl: StructDeclSyntax, + type: some TypeSyntaxProtocol, + context: MacroExpansionContext + ) throws(SerdeDefault.Error) { + // TODO: Support generic types. This needs the generated extension to carry the + // right `where` clauses, so reject it outright instead of expanding to code that + // fails with an unrelated error. + if let genericParameterClause = structDecl.genericParameterClause { + throw SerdeDefault.Error.genericType(genericParameterClause) + } + identifier = structDecl.name + accessLevel = AccessLevel(modifiers: structDecl.modifiers) + let typeSyntax = TypeSyntax(fromProtocol: type) + self.typeSyntax = typeSyntax + let variables = structDecl.memberBlock.members + .compactMap { + SerdeDefault.VariableDecl(declaration: $0.decl) + // Ignore static properties, as they cannot be serialized or deserialized. + } + .filter { !$0.isStatic() } + let bindings = try Self.bindings(variables: variables) + properties = try Properties(bindings: bindings) + // `base` and `serialized` can be spelled literally because property names reach the + // generated function only as member accesses (`value.name`) or dictionary keys. The + // temporaries the properties bind are the part that has to be unique. + serializeContext = SerdeDefault.Serialize.Context( + base: "value", + serialized: IdentifierTypeSyntax.serialized.name, + macroContext: context + ) + deserializeContext = SerdeDefault.Deserialize.Context( + base: context.makeUniqueName("serialized"), + typeSyntax: typeSyntax + ) + } + + /** + Extracts stored bindings from the nominal type variables. + */ + static func bindings(variables: [VariableDecl]) throws(SerdeDefault.Error) -> [PatternBinding] { + try variables + .map { variable throws(Error) -> [PatternBinding] in + try variable.typedBindings() + } + .flatMap { $0 } + .filter { $0.isStored() } + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift new file mode 100644 index 00000000..d64f4a38 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift @@ -0,0 +1,31 @@ +import SwiftSyntax + +extension SerdeDefault { + struct Properties { + let properties: [any Property] + + /** + Initializes properties from the nominal type bindings. + + - Parameter bindings: Pattern bindings from the nominal type. + */ + init(bindings: [PatternBinding]) throws(Error) { + properties = try bindings.map { binding throws(Error) -> any Property in + try StrategyFinder.resolve(binding: binding) + } + } + + /** + Generates a code block for these properties using the given generator. + + - Parameter generator: The property-code generator to use. + - Parameter context: Context passed through to the generator. + */ + func codeGenerated( + using generator: Generator.Type, + in context: Generator.Context + ) -> CodeBlockSyntax { + generator.codeGeneratedBlock(from: properties, in: context) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift new file mode 100644 index 00000000..c7164149 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift @@ -0,0 +1,85 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Property that `Defaults` can serialize and deserialize directly, without a bridge. + + For serialization: + + ```swift + serialized["property"] = value.property + ``` + + For deserialization: + + ```swift + let property = base["property"] as? PropertyType + ``` + */ + struct BasicProperty: Property { + let binding: PatternBinding + let attribute: SerdeAttribute + + static func matches(binding: PatternBinding, attribute _: SerdeAttribute) -> Bool { + binding.typeSyntax.isNativeSupportType() + } + + func serialize(in context: Serialize.Context) -> Serialize.CodeGenerated { + guard !isExcludedFromSerde() else { + return .omitted + } + + // Optional properties are only included in the serialized output if they have a value. + if typeSyntax.isOptional() { + let temporary = context.makeUniqueName(for: identifier) + return .optional( + """ + if let \(temporary) = \(context.base).\(identifier) { + \(context.serialized)["\(identifier)"] = \(temporary) + } + """ + ) + } + + return .basic( + DictionaryElementSyntax( + key: StringLiteralExprSyntax(content: "\(identifier)"), + value: MemberAccessExprSyntax( + base: context.base.declReference(), + name: identifier + ) + ) + ) + } + + func deserialize(in context: Deserialize.Context) -> Deserialize.CodeGenerated { + guard binding.isMemberwiseInitializable() else { + return .omitted + } + + if isExcludedFromSerde() { + return .skipped(identifier: identifier) + } + + let type = unwrappedTypeSyntax + let value: ExprSyntax = "\(context.base)[\(literal: identifier.text)] as? \(type)" + + // A default value wins over optionality, so an optional property with an initializer + // falls back to that initializer rather than to `nil`. + let statement: CodeBlockItemSyntax + if let initializer = binding.syntax.initializer { + statement = "let \(identifier) = \(value) ?? \(initializer.value.trimmed)" + } else if typeSyntax.isOptional() { + statement = "let \(identifier) = \(value)" + } else { + statement = """ + guard let \(identifier) = \(value) else { + return nil + } + """ + } + + return .assignment(identifier: identifier, statement: statement) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift new file mode 100644 index 00000000..abe39453 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift @@ -0,0 +1,74 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Stored property prepared for bridge generation. + + Conformers own the serialization and deserialization code generation for one kind of property, and know whether they apply to a given binding via `matches(binding:attribute:)`. Structure shared by every kind — skip and omission checks, optional handling, `guard` scaffolding — lives in the protocol extension below so conformers only provide what actually varies. + */ + protocol Property { + /** + The pattern binding this property was created from. + */ + var binding: PatternBinding { get } + + /** + Parsed `@serde` attribute arguments for this property. + */ + var attribute: SerdeAttribute { get } + + init(binding: PatternBinding, attribute: SerdeAttribute) + + /** + Returns whether this property kind applies to `binding`. + + - Parameter attribute: Parsed `@serde` arguments for `binding`, so a kind selected by an + attribute rather than by type can recognize itself. + */ + static func matches(binding: PatternBinding, attribute: SerdeAttribute) -> Bool + + /** + Generates the serialization code contributed by this property. + */ + func serialize(in context: Serialize.Context) -> Serialize.CodeGenerated + + /** + Generates the deserialization code contributed by this property. + */ + func deserialize(in context: Deserialize.Context) -> Deserialize.CodeGenerated + } +} + +extension SerdeDefault.Property { + /** + Property identifier used as the serialized key. + */ + var identifier: TokenSyntax { binding.identifier } + + /** + Property type used to choose serialization behavior. + */ + var typeSyntax: TypeSyntax { binding.typeSyntax } + + /** + Property type with any optional wrapper removed. + + Trimmed, since the type is interpolated into generated statements where leftover + trivia from the source (for example, `Int ` in `var a: Int = 1`) would leak through. + */ + var unwrappedTypeSyntax: TypeSyntax { + guard let wrappedType = typeSyntax.optionalWrappedType() else { + return typeSyntax.trimmed + } + return TypeSyntax(fromProtocol: wrappedType).trimmed + } + + /** + Returns whether this property should be excluded from serialization and deserialization. + + Properties marked with `@serde(.skip)` or constants with default values are excluded, as they cannot be reliably deserialized. + */ + func isExcludedFromSerde() -> Bool { + attribute.skip || !binding.isMemberwiseInitializable() + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift new file mode 100644 index 00000000..5d63dc00 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift @@ -0,0 +1,87 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Property that needs `Defaults.Serializable` conversion through its bridge. + + This is the catch-all strategy: `resolve(binding:)` always succeeds, so it must stay + registered last in `StrategyFinder`. + + For serialization: + + ```swift + guard let property = PropertyType.toSerializable(value.property) else { return nil } + serialized["property"] = property + ``` + + For deserialization: + + ```swift + let property = PropertyType.toValue(base["property"] as Any, type: PropertyType.self) + ``` + */ + struct SerializableProperty: Property { + let binding: PatternBinding + let attribute: SerdeAttribute + + static func matches(binding: PatternBinding, attribute _: SerdeAttribute) -> Bool { + !binding.typeSyntax.isNativeSupportType() + } + + func serialize(in context: Serialize.Context) -> Serialize.CodeGenerated { + guard !isExcludedFromSerde() else { + return .omitted + } + + let type = unwrappedTypeSyntax + let temporary = context.makeUniqueName(for: identifier) + if typeSyntax.isOptional() { + return .optional( + """ + if let \(temporary) = \(type).toSerializable(\(context.base).\(identifier)) { + \(context.serialized)["\(identifier)"] = \(temporary) + } + """ + ) + } + + return .serializable( + """ + guard let \(temporary) = \(type).toSerializable(\(context.base).\(identifier)) else { return nil } + \(context.serialized)["\(identifier)"] = \(temporary) + """ + ) + } + + func deserialize(in context: Deserialize.Context) -> Deserialize.CodeGenerated { + guard binding.isMemberwiseInitializable() else { + return .omitted + } + + if isExcludedFromSerde() { + return .skipped(identifier: identifier) + } + + let type = unwrappedTypeSyntax + let value: ExprSyntax = + "\(type).toValue(\(context.base)[\(literal: identifier.text)] as Any, type: \(type).self)" + + // A default value wins over optionality, so an optional property with an initializer + // falls back to that initializer rather than to `nil`. + let statement: CodeBlockItemSyntax + if let initializer = binding.syntax.initializer { + statement = "let \(identifier) = \(value) ?? \(initializer.value.trimmed)" + } else if typeSyntax.isOptional() { + statement = "let \(identifier) = \(value)" + } else { + statement = """ + guard let \(identifier) = \(value) else { + return nil + } + """ + } + + return .assignment(identifier: identifier, statement: statement) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift new file mode 100644 index 00000000..f8d3644a --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift @@ -0,0 +1,58 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Resolves the `Property` implementation for a binding by trying registered strategies in order. + */ + struct StrategyFinder { + /** + Candidate strategies, tried in order. The first one that resolves wins. + + `SerializableProperty` is the catch-all and must stay last. + */ + static var strategies: [any Property.Type] { + [ + BasicProperty.self, + SerializableProperty.self + ] + } + + /** + Resolves the `Property` implementation for `binding`. + + The binding's `@serde` arguments are parsed once up front and handed to every candidate, so + strategies selected by an attribute can recognize themselves without re-parsing. + + - Throws: `Error` if the `@serde` arguments are invalid, or `Error.unresolvedProperty` if no + registered strategy matches the binding. The latter should be unreachable while a catch-all + strategy is registered. + */ + static func resolve(binding: PatternBinding) throws(Error) -> any Property { + let attribute = try SerdeAttribute(binding: binding) + for strategy in strategies where strategy.matches(binding: binding, attribute: attribute) { + return strategy.init(binding: binding, attribute: attribute) + } + throw Error.unresolvedProperty(binding.syntax) + } + + /** + Re-resolves `property` to the one strategy among `strategies` that matches it. + + Reuses the already-parsed binding and attribute, so — unlike `resolve(binding:attribute:)` — + this cannot fail. `property` is returned unchanged when nothing matches. + + - Parameter strategies: Strategies to try, in order. A strategy that wraps the result has to + narrow this to the strategies after itself, or it matches its own binding and recurses. + */ + static func resolve( + property: any Property, + _ strategies: [any Property.Type] = strategies + ) -> any Property { + for strategy in strategies + where strategy.matches(binding: property.binding, attribute: property.attribute) { + return strategy.init(binding: property.binding, attribute: property.attribute) + } + return property + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift new file mode 100644 index 00000000..9046ff18 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift @@ -0,0 +1,100 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Parsed arguments from the `@serde` attribute. + */ + struct SerdeAttribute { + /** + Name of the `@serde` attribute + */ + static let name = IdentifierTypeSyntax.serde.name + + /** + Whether to skip the property during serialization and deserialization. + */ + let skip: Bool + + /** + Parses and validates `@serde` arguments for a property binding. + */ + init(binding: PatternBinding) throws(Error) { + try self.init(attributes: binding.attributes) + if skip && !binding.typeSyntax.isOptional() { + throw Error.skipOnNonOptional(binding.syntax) + } + } + + /** + Parses `@serde` arguments from an attribute list. + + - Parameter attributes: Attributes to inspect. + - Throws: `Error.mismatch` for invalid argument syntax, or + `Error.unknownAttribute` for unsupported arguments. + - Returns: Parsed attribute values. + */ + init(attributes: AttributeListSyntax) throws(Error) { + // Accumulates parsed `@serde` arguments before creating the immutable value. + struct Builder { + var skip: Bool = false + } + var builder = Builder() + for attribute in attributes { + guard + let attribute = attribute.as(AttributeSyntax.self), + case .argumentList(let arguments) = attribute.arguments + else { + continue + } + // Parse arguments only from `@serde(...)` and `@DefaultsMacros.serde(...)`. + guard + attribute.attributeName.isEqual( + to: Self.name, inModule: IdentifierTypeSyntax.DefaultsMacros.name) + else { + continue + } + for argument in arguments { + let name = try Self.findAttributeName(argument: argument) + switch name.text { + case IdentifierTypeSyntax.skip.name.text: + builder.skip = true + default: + throw Error.unknownAttribute(argument.expression, name.text) + } + } + } + + self.skip = builder.skip + } + + /** + Finds the attribute name from a labeled expression. + Accepts `@serde(.skip)` and explicitly qualified `SerdeOption.skip` spellings. + */ + static func findAttributeName(argument: LabeledExprSyntax) throws(Error) -> TokenSyntax { + let memberAccess: MemberAccessExprSyntax + if let base = argument.expression.as(FunctionCallExprSyntax.self)?.calledExpression.as( + MemberAccessExprSyntax.self + ) { + memberAccess = base + } else if let base = argument.expression.as(MemberAccessExprSyntax.self) { + memberAccess = base + } else { + throw Error.mismatch(MemberAccessExprSyntax.self, argument.expression) + } + + if let base = memberAccess.base { + guard + base.isEqual( + to: IdentifierTypeSyntax.SerdeOption.name, + inModule: IdentifierTypeSyntax.DefaultsMacros.name + ) + else { + throw Error.unknownAttribute(argument.expression, memberAccess.trimmedDescription) + } + } + + return memberAccess.declName.baseName + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift new file mode 100644 index 00000000..bc0bc9fa --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift @@ -0,0 +1,121 @@ +import SwiftDiagnostics +import SwiftSyntax + +extension SerdeDefault { + /** + Diagnostics emitted by `SerdeDefaultMacro`. + */ + enum Error: Swift.Error { + /** + The declaration is not a supported nominal type (`struct` only). + */ + case unexpectedNominalType(any SyntaxProtocol) + /** + The nominal declaration has generic parameters. + */ + case genericType(any SyntaxProtocol) + /** + The syntax node does not match the expected type. + */ + case mismatch(Any.Type, any SyntaxProtocol) + /** + The property binding is missing a type annotation. + */ + case missingType(any SyntaxProtocol) + /** + The stored property type could not be inferred from its initializer. + */ + case inferredType(any SyntaxProtocol) + /** + A non-optional property is marked with `@serde(.skip)`, which is unsupported. + */ + case skipOnNonOptional(any SyntaxProtocol) + /** + An attribute is not recognized. + */ + case unknownAttribute(any SyntaxProtocol, String) + /** + No registered `Property` strategy could resolve the binding. + + This should be unreachable while a catch-all strategy (`SerializableProperty`) is registered. + */ + case unresolvedProperty(any SyntaxProtocol) + + func syntax() -> any SyntaxProtocol { + switch self { + case .unexpectedNominalType(let got): + got + case .genericType(let got): + got + case .mismatch(_, let got): + got + case .missingType(let got): + got + case .inferredType(let got): + got + case .skipOnNonOptional(let got): + got + case .unknownAttribute(let got, _): + got + case .unresolvedProperty(let got): + got + } + } + + var caseName: String { + switch self { + case .unexpectedNominalType: + "unexpectedNominalType" + case .genericType: + "genericType" + case .mismatch: + "mismatch" + case .missingType: + "missingType" + case .inferredType: + "inferredType" + case .skipOnNonOptional: + "skipOnNonOptional" + case .unknownAttribute: + "unknownAttribute" + case .unresolvedProperty: + "unresolvedProperty" + } + } + } +} + +extension SerdeDefault.Error: DiagnosticMessage { + var message: String { + switch self { + case .unexpectedNominalType(let got): + "Expected a struct declaration. Received: \(got.syntaxNodeType)." + + " In this first-stage implementation, `@SerdeDefault` supports structs only." + case .genericType: + "`@SerdeDefault` does not support generic types. Apply it to a concrete struct instead." + case .mismatch(let expected, let got): + "Expected syntax node type: \(expected). Received: \(got.syntaxNodeType)." + case .missingType: + "Missing type annotation for `@SerdeDefault` stored property." + + " Add an explicit type annotation or use an initializer with a supported literal type." + case .inferredType: + "Could not infer the type of `@SerdeDefault` stored property initializer." + + " Add an explicit type annotation." + case .skipOnNonOptional: + "Non-optional property is marked with `@serde(.skip)`, which is not supported." + + " Please remove the `skip` argument or make the property optional." + case .unknownAttribute(_, let name): + "Unknown attribute found: \(name)." + case .unresolvedProperty: + "Could not resolve a serialization strategy for this property." + } + } + + var diagnosticID: SwiftDiagnostics.MessageID { + MessageID(domain: "com.sindresorhus.SerdeDefaultMacro", id: "\(caseName)") + } + + var severity: SwiftDiagnostics.DiagnosticSeverity { + .error + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/AccessLevel.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/AccessLevel.swift new file mode 100644 index 00000000..7ada99f1 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/AccessLevel.swift @@ -0,0 +1,44 @@ +import SwiftSyntax + +/** +Swift access levels used for generated declarations. +*/ +enum AccessLevel: CaseIterable, Comparable, Sendable { + case `private` + case `fileprivate` + case `internal` + case `package` + case `public` + + init(modifiers: DeclModifierListSyntax) { + for modifier in modifiers { + if let accessLevel = Self.allCases.first(where: { $0.token.text == modifier.name.text }) { + self = accessLevel + return + } + } + self = .internal + } + + var declModifier: DeclModifierSyntax { + DeclModifierSyntax(name: self.token) + } + + /** + The SwiftSyntax token for the access level. + */ + var token: TokenSyntax { + switch self { + case .`public`: + return .keyword(.public) + case .`package`: + return .keyword(.package) + case .`internal`: + return .keyword(.internal) + case .`fileprivate`: + return .keyword(.fileprivate) + case .`private`: + return .keyword(.private) + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift new file mode 100644 index 00000000..78233d0b --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift @@ -0,0 +1,78 @@ +import SwiftSyntax + +extension SwiftSyntax.IdentifierTypeSyntax { + // MARK: Identifiers for generated `Defaults.Serializable` conformance + static let Defaults = Self(name: "Defaults") + static let Serializable = Self(name: "Serializable") + static let Bridge = Self(name: "Bridge") + static let Value = Self(name: "Value") + static let bridge = Self(name: "bridge") + static let serialize = Self(name: "serialize") + static let deserialize = Self(name: "deserialize") + static let serialized = Self(name: "serialized") + + // MARK: Identifiers for Swift and Foundation types + static let Swift = Self(name: "Swift") + static let `Optional` = Self(name: "Optional") + static let `Any` = Self(name: "Any") + static let Sendable = Self(name: "Sendable") + static let Bool = Self(name: "Bool") + static let Int = Self(name: "Int") + static let UInt = Self(name: "UInt") + static let Double = Self(name: "Double") + static let Float = Self(name: "Float") + static let String = Self(name: "String") + static let Data = Self(name: "Data") + static let Date = Self(name: "Date") + static let CGFloat = Self(name: "CGFloat") + static let Int8 = Self(name: "Int8") + static let UInt8 = Self(name: "UInt8") + static let Int16 = Self(name: "Int16") + static let UInt16 = Self(name: "UInt16") + static let Int32 = Self(name: "Int32") + static let UInt32 = Self(name: "UInt32") + static let Int64 = Self(name: "Int64") + static let UInt64 = Self(name: "UInt64") + + /** + Identifier for the `@serde` attribute. + */ + static let serde = Self(name: "serde") + /** + Identifier for the `DefaultsMacros` module used to qualify `SerdeOption`. + */ + static let DefaultsMacros = Self(name: "DefaultsMacros") + /** + Identifier for the `SerdeOption` enum used by `@serde`. + */ + static let SerdeOption = Self(name: "SerdeOption") + /** + Identifier for the `skip` argument used in `@serde(.skip)`. + */ + static let skip = Self(name: "skip") + + /** + Types that `Defaults` stores in `UserDefaults` without a bridge. + + Keep this in sync with the `isNativelySupportedType = true` declarations in `Defaults+Extensions.swift`. + */ + static let nativeSupportedTypes: [Self] = [ + .Bool, + .Int, + .UInt, + .Double, + .Float, + .String, + .Data, + .Date, + .CGFloat, + .Int8, + .UInt8, + .Int16, + .UInt16, + .Int32, + .UInt32, + .Int64, + .UInt64 + ] +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift new file mode 100644 index 00000000..20dcfc91 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift @@ -0,0 +1,22 @@ +import SwiftSyntax + +extension SwiftSyntax.MemberTypeSyntax { + /** + Type syntax for `DefaultsMacros.SerdeOption`. + */ + static let DefaultsMacrosSerdeOption = Self( + baseType: SwiftSyntax.IdentifierTypeSyntax.DefaultsMacros, + name: SwiftSyntax.IdentifierTypeSyntax.SerdeOption.name) + /** + Type syntax for `Defaults.Bridge`. + */ + static let DefaultsBridge = Self( + baseType: SwiftSyntax.IdentifierTypeSyntax.Defaults, + name: SwiftSyntax.IdentifierTypeSyntax.Bridge.name) + /** + Type syntax for `Defaults.Serializable`. + */ + static let DefaultsSerializable = Self( + baseType: SwiftSyntax.IdentifierTypeSyntax.Defaults, + name: SwiftSyntax.IdentifierTypeSyntax.Serializable.name) +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift new file mode 100644 index 00000000..f957aa00 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +extension SwiftSyntax.DeclModifierSyntax { + /** + Reusable `public` modifier for generated declarations. + */ + static let `public` = Self(name: .keyword(.public)) + /** + Reusable `static` modifier for generated declarations. + */ + static let `static` = Self(name: .keyword(.static)) +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift new file mode 100644 index 00000000..40bc2a4d --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift @@ -0,0 +1,72 @@ +import SwiftSyntax + +extension ExprSyntax { + /** + Infers a Swift type from simple literals and type expressions. + */ + func inferredType() -> (any TypeSyntaxProtocol)? { + switch self.as(ExprSyntaxEnum.self) { + case .prefixOperatorExpr(let prefixExpr): + switch prefixExpr.operator.text { + case "+", "-": + return prefixExpr.expression.inferredType() + default: + return nil + } + + case .tupleExpr(let tupleExpr): + guard + tupleExpr.elements.count == 1, + let element = tupleExpr.elements.first, + element.label == nil + else { + return nil + } + + return element.expression.inferredType() + + case .booleanLiteralExpr: + return IdentifierTypeSyntax.Bool + + case .integerLiteralExpr: + return IdentifierTypeSyntax.Int + + case .floatLiteralExpr: + return IdentifierTypeSyntax.Double + + case .stringLiteralExpr: + return IdentifierTypeSyntax.String + + case .typeExpr(let typeExpr): + return typeExpr.type + + case .arrayExpr(let arrayExpr): + guard + let firstElement = arrayExpr.elements.first, + let type = firstElement.expression.inferredType() + else { + return nil + } + + return ArrayTypeSyntax(element: TypeSyntax(fromProtocol: type)) + + case .dictionaryExpr(let dictionaryExpr): + guard + case .elements(let elements) = dictionaryExpr.content, + let firstElement = elements.first, + let keyType = firstElement.key.inferredType(), + let valueType = firstElement.value.inferredType() + else { + return nil + } + + return DictionaryTypeSyntax( + key: TypeSyntax(fromProtocol: keyType), + value: TypeSyntax(fromProtocol: valueType) + ) + + default: + return nil + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift new file mode 100644 index 00000000..1180a83a --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift @@ -0,0 +1,24 @@ +import SwiftSyntax + +extension SerdeDefault { + /** + Supports wrapping syntax nodes in `OptionalTypeSyntax`. + */ + protocol OptionalType { + /** + Wraps the type in `OptionalTypeSyntax`. + */ + func optional() -> OptionalTypeSyntax + } +} + +extension SerdeDefault.OptionalType where Self: TypeSyntaxProtocol { + func optional() -> OptionalTypeSyntax { + OptionalTypeSyntax(wrappedType: self) + } +} + +/** +Allows identifier types such as `String` and `Int` to be wrapped in `OptionalTypeSyntax`. +*/ +extension IdentifierTypeSyntax: SerdeDefault.OptionalType {} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift new file mode 100644 index 00000000..542d9b9a --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift @@ -0,0 +1,36 @@ +import SwiftSyntax + +extension SyntaxProtocol { + /** + Returns whether the syntax is equal to the given name and module. + + - Parameter name: The name to compare against. + - Parameter module: The module to compare against. + - Returns: `true` if the syntax is equal to the name and module, + `false` otherwise. + */ + func isEqual(to name: TokenSyntax, inModule module: TokenSyntax) -> Bool { + switch Syntax(self).as(SyntaxEnum.self) { + case .identifierType(let identifier): + return identifier.name.text == name.text + case .declReferenceExpr(let reference): + return reference.baseName.text == name.text + case .memberType(let memberType): + guard let baseType = memberType.baseType.as(IdentifierTypeSyntax.self) else { + return false + } + + return memberType.name.text == name.text + && baseType.name.text == module.text + case .memberAccessExpr(let memberAccess): + guard let moduleReference = memberAccess.base?.as(DeclReferenceExprSyntax.self) else { + return false + } + + return memberAccess.declName.baseName.text == name.text + && moduleReference.baseName.text == module.text + default: + return false + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift new file mode 100644 index 00000000..6db713e4 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift @@ -0,0 +1,23 @@ +import SwiftSyntax + +extension TokenSyntax { + /** + Creates a declaration-reference expression from the token text. + */ + func declReference() -> DeclReferenceExprSyntax { + DeclReferenceExprSyntax(leadingTrivia: nil, baseName: self.trimmed, trailingTrivia: nil) + } + + func prefix(prefix: String) -> Self { + TokenSyntax(stringLiteral: "\(prefix)\(self.text)") + } + + /** + Returns a token with `suffix` appended. + + - Parameter suffix: The suffix to append. + */ + func suffix(_ suffix: String) -> Self { + TokenSyntax(stringLiteral: "\(self.text)\(suffix)") + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift new file mode 100644 index 00000000..dcd40ffb --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift @@ -0,0 +1,118 @@ +import SwiftSyntax + +extension TypeSyntaxProtocol { + /** + Returns generic arguments from either: + + - `IdentifierTypeSyntax` (for example, `Optional`) + - `MemberTypeSyntax` (for example, `Swift.Optional`) + + Non-generic types return an empty array. + */ + func genericArguments() -> [GenericArgumentSyntax] { + if let memberType = self.as(MemberTypeSyntax.self) { + guard let genericArgumentClause = memberType.genericArgumentClause else { + return [] + } + return genericArgumentClause.arguments.map { GenericArgumentSyntax(argument: $0.argument) } + } + if let identifierType = self.as(IdentifierTypeSyntax.self) { + guard let genericArgumentClause = identifierType.genericArgumentClause else { + return [] + } + return genericArgumentClause.arguments.map { GenericArgumentSyntax(argument: $0.argument) } + } + return [] + } + + /** + Returns the wrapped type for any supported optional spelling: + + - `T?` + - `Optional` + - `Swift.Optional` +cv + Generated `toValue` calls use this to convert to `T` instead of producing nested optionals. + */ + func optionalWrappedType() -> (any TypeSyntaxProtocol)? { + if let optionalType = self.as(OptionalTypeSyntax.self) { + return optionalType.wrappedType + } + if self.isSwiftMemberGenericOptional() || self.isGenericOptional() { + if let genericArgument = self.genericArguments().first { + // `Optional` should always carry a type argument. + if case .type(let type) = genericArgument.argument { + return type + } + } + } + + return nil + } + + /** + Returns whether the type is exactly `Swift.Optional`. + + This explicit check avoids accidentally matching unrelated `Optional` symbols + from other modules or nested types. + */ + func isSwiftMemberGenericOptional() -> Bool { + if let memberType = self.as(MemberTypeSyntax.self) { + if let identifierType = memberType.baseType.as(IdentifierTypeSyntax.self) { + return identifierType.name.text == IdentifierTypeSyntax.Swift.name.text + && memberType.name.text == IdentifierTypeSyntax.Optional.name.text + } + } + return false + } + + /** + Returns whether the type is unqualified `Optional`. + */ + func isGenericOptional() -> Bool { + if let identifierType = self.as(IdentifierTypeSyntax.self) { + if identifierType.name.text == IdentifierTypeSyntax.Optional.name.text { + return true + } + } + return false + } + + /** + Returns whether the type is any supported optional spelling. + */ + func isOptional() -> Bool { + self.is(OptionalTypeSyntax.self) || self.isGenericOptional() + || self.isSwiftMemberGenericOptional() + } + + /** + Returns whether `Defaults` can store this type without a bridge. + */ + func isNativeSupportType() -> Bool { + if let wrappedType = optionalWrappedType() { + return wrappedType.isNativeSupportType() + } + + switch TypeSyntax(self).as(TypeSyntaxEnum.self) { + case .arrayType(let arrayType): + return arrayType.element.isNativeSupportType() + case .dictionaryType(let dictionaryType): + guard + let keyType = dictionaryType.key.as(IdentifierTypeSyntax.self), + keyType.name.text == IdentifierTypeSyntax.String.name.text + else { + return false + } + return dictionaryType.value.isNativeSupportType() + case .memberType(let memberType): + return IdentifierTypeSyntax(name: memberType.name).isNativeSupportType() + case .identifierType(let identifierType): + return IdentifierTypeSyntax.nativeSupportedTypes.contains { + $0.name.text == identifierType.name.text + } + default: + return false + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift new file mode 100644 index 00000000..358ebebb --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift @@ -0,0 +1,89 @@ +import SwiftSyntax + +extension SerdeDefault { + struct FunctionDecl { + let syntax: FunctionDeclSyntax + + class Builder { + /** + The generated function name. + */ + let name: TokenSyntax + /** + The generated function access level. + */ + var accessLevel: AccessLevel = .internal + /** + Parameters collected for the function signature. + */ + var parameters: [FunctionParameterSyntax] = [] + var codeBlockItems: [CodeBlockItemSyntax] = [] + var returnType: ReturnClauseSyntax? + + init(name: TokenSyntax) { + self.name = name + } + + func setAccessLevel(accessLevel: AccessLevel) -> Self { + self.accessLevel = accessLevel + return self + } + + /** + Adds a parameter to the function signature. + + - Parameter firstName: External parameter name. + - Parameter secondName: Internal parameter name. + - Parameter type: Parameter type. + */ + func addParameter( + firstName: TokenSyntax = .wildcardToken(), + secondName: TokenSyntax, + type: some TypeSyntaxProtocol + ) -> Self { + self.parameters.append( + FunctionParameterSyntax( + firstName: firstName, + secondName: secondName, + type: type + )) + return self + } + + /** + Sets the return type for the function signature. + + - Parameter returnType: The return type syntax. + */ + func setReturnType(returnType: some TypeSyntaxProtocol) -> Self { + self.returnType = ReturnClauseSyntax(type: returnType) + return self + } + + /** + Builds the final function declaration. + + - Returns: The generated function declaration. + */ + func build(_ codeBlock: ([FunctionParameterSyntax]) -> CodeBlockSyntax) + -> FunctionDeclSyntax { + let functionParameters = FunctionParameterClauseSyntax( + parameters: FunctionParameterListSyntax(parameters) + ) + + let body = codeBlock(parameters) + let functionSignature = FunctionSignatureSyntax( + parameterClause: functionParameters, + returnClause: returnType + ) + + return FunctionDeclSyntax( + modifiers: [accessLevel.declModifier], + name: name, + signature: functionSignature, + body: body, + ) + } + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift new file mode 100644 index 00000000..2cf42a96 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift @@ -0,0 +1,71 @@ +import SwiftSyntax + +extension SerdeDefault { + struct PatternBinding { + let specifier: TokenSyntax + let syntax: PatternBindingSyntax + let identifier: TokenSyntax + let typeSyntax: TypeSyntax + let attributes: AttributeListSyntax + + init( + specifier: TokenSyntax, + syntax: PatternBindingSyntax, + type: some TypeSyntaxProtocol, + attributes: AttributeListSyntax? = nil + ) throws(SerdeDefault.Error) { + self.specifier = specifier + self.syntax = syntax + self.identifier = try Self.identifier(in: syntax) + self.typeSyntax = TypeSyntax(fromProtocol: type) + self.attributes = attributes ?? [] + } + + /** + Extracts the identifier from a pattern binding. + + - Throws: `SerdeDefault.Error.mismatch` when the pattern is not an identifier. + */ + static func identifier(in syntax: PatternBindingSyntax) throws(SerdeDefault.Error) + -> TokenSyntax { + guard let pattern = syntax.pattern.as(IdentifierPatternSyntax.self) else { + throw SerdeDefault.Error.mismatch(IdentifierPatternSyntax.self, syntax.pattern) + } + return pattern.identifier.trimmed + } + + /** + Returns whether this binding can be initialized with the memberwise initializer. + */ + func isMemberwiseInitializable() -> Bool { + isStored() + && !(specifier.tokenKind == .keyword(.let) && syntax.initializer != nil) + } + + /** + Returns whether this binding represents a stored property. + + Computed properties with a getter are excluded. + Observed properties (`willSet`/`didSet`) are treated as stored. + */ + func isStored() -> Bool { + // Bindings without an accessor block are stored properties. + guard let accessorBlock = self.syntax.accessorBlock else { + return true + } + switch accessorBlock.accessors { + case .getter: + return false + case .accessors(let accessors): + return accessors.allSatisfy { accessor in + switch accessor.accessorSpecifier.tokenKind { + case .keyword(.willSet), .keyword(.didSet): + return true + default: + return false + } + } + } + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift new file mode 100644 index 00000000..4fe83004 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift @@ -0,0 +1,72 @@ +import SwiftSyntax + +extension SerdeDefault { + struct VariableDecl { + let syntax: VariableDeclSyntax + + init?(declaration: some DeclSyntaxProtocol) { + guard let syntax = declaration.as(VariableDeclSyntax.self) else { + return nil + } + self.syntax = syntax + } + + /** + Returns whether the variable declaration is static. + */ + func isStatic() -> Bool { + syntax.modifiers.contains { modifier in + modifier.name.text == DeclModifierSyntax.static.name.text + } + } + + /** + Extracts typed bindings from the variable declaration. + */ + func typedBindings() throws(Error) -> [PatternBinding] { + let specifier = self.syntax.bindingSpecifier + let attributes = self.syntax.attributes + + return try self.syntax.bindings.enumerated().map { index, binding throws(Error) in + let type = try self.type(for: binding, at: index) + return try PatternBinding( + specifier: specifier, syntax: binding, type: type, attributes: attributes + ) + } + } + + /** + Resolves the type for the binding at `index`. + + If the binding has an initializer, simple literal types are inferred from it. + Otherwise, this falls back to the next explicit type in the same declaration + (for example, `var a, b: Int`). + */ + private func type(for binding: PatternBindingSyntax, at index: Int) throws(Error) -> TypeSyntax { + if let typeSyntax = binding.typeAnnotation?.type { + return typeSyntax + } + + if let initializer = binding.initializer { + guard let inferredType = initializer.value.inferredType() else { + throw Error.inferredType(initializer.value) + } + + return TypeSyntax(fromProtocol: inferredType) + } + + guard + let typeSyntax = self.syntax.bindings + .map(\.typeAnnotation?.type) + .dropFirst(index) + .lazy + .compactMap({ $0 }) + .first + else { + throw Error.missingType(binding) + } + + return typeSyntax + } + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift new file mode 100644 index 00000000..934a4ee8 --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift @@ -0,0 +1,57 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +/// Macro declaration for the ``SerdeDefault`` macro. +public struct SerdeDefaultMacro {} + +enum SerdeDefault {} + +extension SerdeDefaultMacro: ExtensionMacro { + /** + Generates the extension declaration that adds `Defaults.Serializable` conformance. + + - Parameter declaration: The declaration annotated with `@SerdeDefault`. + - Parameter type: The concrete type being extended. + + - Returns: The generated extension declaration. + */ + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, + conformingTo protocols: [SwiftSyntax.TypeSyntax], + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + let nominalDecl: SerdeDefault.NominalDecl + do { + nominalDecl = try SerdeDefault.NominalDecl( + declaration: declaration, type: type, macroContext: context) + } catch { + context.diagnose(Diagnostic(node: Syntax(fromProtocol: error.syntax()), message: error)) + return [] + } + let inheritanceClause = InheritanceClauseSyntax( + inheritedTypes: InheritedTypeListSyntax([ + InheritedTypeSyntax(type: MemberTypeSyntax.DefaultsSerializable) + ]) + ) + + // Generate the bridge type and store an instance on `static let bridge`. + let bridgeStruct = nominalDecl.bridgeStructDecl() + let bridgeVariable = nominalDecl.bridgeVariableDecl() + + let memberBlock = MemberBlockSyntax( + members: MemberBlockItemListSyntax([ + MemberBlockItemSyntax(decl: DeclSyntax(bridgeStruct)), + MemberBlockItemSyntax(decl: DeclSyntax(bridgeVariable)) + ]) + ) + let extensionDecl = ExtensionDeclSyntax( + extendedType: nominalDecl.typeSyntax, + inheritanceClause: inheritanceClause, + memberBlock: memberBlock + ) + return [extensionDecl] + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift b/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift new file mode 100644 index 00000000..d69430be --- /dev/null +++ b/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift @@ -0,0 +1,21 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +/** +Macro declaration for the ``serde`` macro. +*/ +public struct SerdeMacro {} + +extension SerdeMacro: PeerMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + guard SerdeDefault.VariableDecl(declaration: declaration) != nil else { + throw SerdeDefault.Error.mismatch(VariableDeclSyntax.self, declaration) + } + + return [] + } +} diff --git a/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift index 3ecaca0b..dca9d9c0 100644 --- a/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/ObservableDefaultMacroTests.swift @@ -8,11 +8,11 @@ import XCTest @testable import DefaultsMacros @testable import DefaultsMacrosDeclarations -let testMacros: [String: Macro.Type] = [ +private let testMacros: [String: Macro.Type] = [ "ObservableDefault": ObservableDefaultMacro.self ] #else -let testMacros: [String: Macro.Type] = [:] +private let testMacros: [String: Macro.Type] = [:] #endif final class ObservableDefaultMacroTests: XCTestCase { diff --git a/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift new file mode 100644 index 00000000..1e9bfd9d --- /dev/null +++ b/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift @@ -0,0 +1,784 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +@testable import Defaults + +// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. +// Cross-compiled tests may still make use of the macro itself in end-to-end tests. +#if canImport(DefaultsMacrosDeclarations) + @testable import DefaultsMacros + @testable import DefaultsMacrosDeclarations + + private let testMacros: [String: Macro.Type] = [ + "SerdeDefault": SerdeDefaultMacro.self, + "serde": SerdeMacro.self + ] + + private enum Group { + case standalone + } + + private enum Role: String, Defaults.Serializable { + case admin = "Admin" + case guest = "Guest" + } + + // @SerdeDefault + // private struct User { + // let name: String + // let age: UInt? + // let role: Role? + // @serde(.skip) + // let group: Group? + // } +#else + private let testMacros: [String: Macro.Type] = [:] +#endif + +final class SerdeDefaultMacroTests: XCTestCase { + private let suite = UserDefaults(suiteName: UUID().uuidString)! + + deinit { + Defaults.removeAll(suite: suite) + } + + func testExpansionWithMemberSyntax() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + var age: UInt + } + """#, + expandedSource: #""" + struct User { + var name: String + var age: UInt + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name, "age": value.age] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + guard let age = __macro_local_10serializedfMu_["age"] as? UInt else { + return nil + } + return User(name: name, age: age) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionWithOptionalMemberType() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + var age: UInt? + } + """#, + expandedSource: #""" + struct User { + var name: String + var age: UInt? + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + var serialized: Serializable = ["name": value.name] + if let __macro_local_3agefMu_ = value.age { + serialized["age"] = __macro_local_3agefMu_ + } + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + let age = __macro_local_10serializedfMu_["age"] as? UInt + return User(name: name, age: age) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionWithQualifiedSerdeAttribute() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + @DefaultsMacros.serde(.skip) + var group: Group? + } + """#, + expandedSource: #""" + struct User { + var name: String + @DefaultsMacros.serde(.skip) + var group: Group? + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + return User(name: name, group: nil) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionWithQualifiedSerdeOptions() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + @serde(SerdeOption.skip) + var group: Group? + @serde(DefaultsMacros.SerdeOption.skip) + var role: Role? + } + """#, + expandedSource: #""" + struct User { + var name: String + var group: Group? + var role: Role? + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + return User(name: name, group: nil, role: nil) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionKeepsDeserializeInitializerArgumentsTogether() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + @DefaultsMacros.serde(.skip) + var group: Group? + let version = 1 + } + """#, + expandedSource: #""" + struct User { + var name: String + @DefaultsMacros.serde(.skip) + var group: Group? + let version = 1 + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + return User(name: name, group: nil) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionInfersLiteralMemberTypes() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Settings { + var name = "Alice" + var isEnabled = true + var count = 1 + var ratio = 0.5 + } + """#, + expandedSource: #""" + struct Settings { + var name = "Alice" + var isEnabled = true + var count = 1 + var ratio = 0.5 + } + + extension Settings: Defaults.Serializable { + struct SettingsBridge: Defaults.Bridge, Sendable { + typealias Value = Settings + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name, "isEnabled": value.isEnabled, "count": value.count, "ratio": value.ratio] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let name = __macro_local_10serializedfMu_["name"] as? String ?? "Alice" + let isEnabled = __macro_local_10serializedfMu_["isEnabled"] as? Bool ?? true + let count = __macro_local_10serializedfMu_["count"] as? Int ?? 1 + let ratio = __macro_local_10serializedfMu_["ratio"] as? Double ?? 0.5 + return Settings(name: name, isEnabled: isEnabled, count: count, ratio: ratio) + } + } + static let bridge = SettingsBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionInfersWrappedAndSignedLiteralMemberTypes() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Settings { + var negativeCount = -1 + var positiveRatio = +0.5 + var parenthesizedName = ("Alice") + } + """#, + expandedSource: #""" + struct Settings { + var negativeCount = -1 + var positiveRatio = +0.5 + var parenthesizedName = ("Alice") + } + + extension Settings: Defaults.Serializable { + struct SettingsBridge: Defaults.Bridge, Sendable { + typealias Value = Settings + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["negativeCount": value.negativeCount, "positiveRatio": value.positiveRatio, "parenthesizedName": value.parenthesizedName] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let negativeCount = __macro_local_10serializedfMu_["negativeCount"] as? Int ?? -1 + let positiveRatio = __macro_local_10serializedfMu_["positiveRatio"] as? Double ?? +0.5 + let parenthesizedName = __macro_local_10serializedfMu_["parenthesizedName"] as? String ?? ("Alice") + return Settings(negativeCount: negativeCount, positiveRatio: positiveRatio, parenthesizedName: parenthesizedName) + } + } + static let bridge = SettingsBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionInfersCollectionLiteralMemberTypes() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Settings { + var tags = ["swift"] + var scores = ["home": 1] + } + """#, + expandedSource: #""" + struct Settings { + var tags = ["swift"] + var scores = ["home": 1] + } + + extension Settings: Defaults.Serializable { + struct SettingsBridge: Defaults.Bridge, Sendable { + typealias Value = Settings + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["tags": value.tags, "scores": value.scores] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let tags = __macro_local_10serializedfMu_["tags"] as? [String] ?? ["swift"] + let scores = __macro_local_10serializedfMu_["scores"] as? [String: Int] ?? ["home": 1] + return Settings(tags: tags, scores: scores) + } + } + static let bridge = SettingsBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionWithNonNativeMemberType() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Profile { + var name: String + var role: Role + } + """#, + expandedSource: #""" + struct Profile { + var name: String + var role: Role + } + + extension Profile: Defaults.Serializable { + struct ProfileBridge: Defaults.Bridge, Sendable { + typealias Value = Profile + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + var serialized: Serializable = ["name": value.name] + guard let __macro_local_4rolefMu_ = Role.toSerializable(value.role) else { + return nil + } + serialized["role"] = __macro_local_4rolefMu_ + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + guard let role = Role.toValue(__macro_local_10serializedfMu_["role"] as Any, type: Role.self) else { + return nil + } + return Profile(name: name, role: role) + } + } + static let bridge = ProfileBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionWithOptionalNonNativeMemberType() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Profile { + var name: String + var role: Role? + } + """#, + expandedSource: #""" + struct Profile { + var name: String + var role: Role? + } + + extension Profile: Defaults.Serializable { + struct ProfileBridge: Defaults.Bridge, Sendable { + typealias Value = Profile + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + var serialized: Serializable = ["name": value.name] + if let __macro_local_4rolefMu_ = Role.toSerializable(value.role) { + serialized["role"] = __macro_local_4rolefMu_ + } + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + let role = Role.toValue(__macro_local_10serializedfMu_["role"] as Any, type: Role.self) + return Profile(name: name, role: role) + } + } + static let bridge = ProfileBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionUsesMatchingInitializerForMultiBindingDeclaration() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Pair { + var first: Int, second: Int = 1 + } + """#, + expandedSource: #""" + struct Pair { + var first: Int, second: Int = 1 + } + + extension Pair: Defaults.Serializable { + struct PairBridge: Defaults.Bridge, Sendable { + typealias Value = Pair + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["first": value.first, "second": value.second] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let first = __macro_local_10serializedfMu_["first"] as? Int else { + return nil + } + let second = __macro_local_10serializedfMu_["second"] as? Int ?? 1 + return Pair(first: first, second: second) + } + } + static let bridge = PairBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionIgnoresComputedProperties() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String + var displayName: String { + name.uppercased() + } + } + """#, + expandedSource: #""" + struct User { + var name: String + var displayName: String { + name.uppercased() + } + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["name": value.name] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + return User(name: name) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionDiagnosesSkipOnNonOptionalProperty() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + @serde(.skip) + var name: String + } + """#, + expandedSource: #""" + struct User { + var name: String + } + """#, + diagnostics: [ + DiagnosticSpec( + message: "Non-optional property is marked with `@serde(.skip)`, which is not supported. Please remove the `skip` argument or make the property optional.", + line: 4, + column: 6 + ) + ], + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionDiagnosesGenericStruct() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Box { + var value: T + } + """#, + expandedSource: #""" + struct Box { + var value: T + } + """#, + diagnostics: [ + DiagnosticSpec( + message: "`@SerdeDefault` does not support generic types. Apply it to a concrete struct instead.", + line: 2, + column: 11 + ) + ], + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + /** + A property named after one of the identifiers the generated `serialize` declares would + shadow it, so the temporaries it binds have to be unique names. + */ + func testExpansionKeepsSerializeHygienicForCollidingPropertyNames() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var serialized: String? + var name: String + } + """#, + expandedSource: #""" + struct User { + var serialized: String? + var name: String + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + var serialized: Serializable = ["name": value.name] + if let __macro_local_10serializedfMu0_ = value.serialized { + serialized["serialized"] = __macro_local_10serializedfMu0_ + } + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let serialized = __macro_local_10serializedfMu_["serialized"] as? String + guard let name = __macro_local_10serializedfMu_["name"] as? String else { + return nil + } + return User(serialized: serialized, name: name) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionUsesPropertyDefaultsWhenDeserializationReturnsNil() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct User { + var name: String = "Anonymous" + var role: Role = .guest + } + """#, + expandedSource: #""" + struct User { + var name: String = "Anonymous" + var role: Role = .guest + } + + extension User: Defaults.Serializable { + struct UserBridge: Defaults.Bridge, Sendable { + typealias Value = User + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + var serialized: Serializable = ["name": value.name] + guard let __macro_local_4rolefMu_ = Role.toSerializable(value.role) else { + return nil + } + serialized["role"] = __macro_local_4rolefMu_ + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let name = __macro_local_10serializedfMu_["name"] as? String ?? "Anonymous" + let role = Role.toValue(__macro_local_10serializedfMu_["role"] as Any, type: Role.self) ?? .guest + return User(name: name, role: role) + } + } + static let bridge = UserBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + // func testDefaultsKeyUsage() throws { + // #if canImport(DefaultsMacrosDeclarations) + // let defaultUser = User(name: "Alice", age: 30, role: .admin, group: .standalone) + // let key = Defaults.Key("serdeDefaultsKeyUsage", default: defaultUser, suite: suite) + // XCTAssertEqual(Defaults[key].name, defaultUser.name) + // let newUser = User(name: "Bob", age: nil, role: .guest, group: .standalone) + // Defaults[key] = newUser + // XCTAssertEqual(Defaults[key].name, newUser.name) + // XCTAssertEqual(Defaults[key].age, newUser.age) + // XCTAssertEqual(Defaults[key].role, newUser.role) + // #else + // throw XCTSkip("Macros are only supported when running tests for the host platform") + // #endif + // } +} diff --git a/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift b/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift index 0d3df98e..3df5ccd1 100644 --- a/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift +++ b/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift @@ -2,7 +2,7 @@ import SwiftUI import Testing import Defaults -private let suite_ = createSuite() +private let suite_: UserDefaults = createSuite() private enum mime: String, Defaults.Serializable { case JSON = "application/json" diff --git a/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift index 2ed693e3..7a169e0c 100644 --- a/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift +++ b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift @@ -33,7 +33,6 @@ public final class DefaultsUserBridge: Defaults.Bridge { else { return nil } - return User(username: username, password: password) } } From ccc7f5804ab55523efa99089831a6cff760ad35a Mon Sep 17 00:00:00 2001 From: hank121314 Date: Tue, 15 Sep 2026 01:04:39 +0800 Subject: [PATCH 2/4] fix: self-review feedback --- Sources/DefaultsMacros/SerdeDefault.swift | 56 +++---- .../CodeBlock/Deserialize.swift | 6 +- .../CodeGenerating/CodeBlock/Serialize.swift | 15 +- .../CodeGenerating/CodeGenerating.swift | 2 +- .../Semantics/NominalDecl+Bridge.swift | 36 ++--- .../SerdeDefault/Semantics/NominalDecl.swift | 20 +-- .../SerdeDefault/Semantics/Properties.swift | 6 +- .../Semantics/Properties/BasicProperty.swift | 8 +- .../Semantics/Properties/Property.swift | 32 ++-- .../Properties/SerializableProperty.swift | 11 +- .../Semantics/Properties/StrategyFinder.swift | 31 +--- .../Semantics/SerdeAttribute.swift | 15 +- .../SerdeDefault/SerdeDefault+Error.swift | 15 ++ .../Constants/IdentifierTypeSyntax.swift | 5 + .../Syntax/Constants/MemberTypeSyntax.swift | 2 + .../Extensions/DeclModifierSyntax.swift | 1 + .../Syntax/Extensions/ExprSyntax.swift | 66 ++++++--- .../Syntax/Extensions/OptionalType.swift | 4 +- .../Syntax/Extensions/SyntaxProtocol.swift | 5 +- .../Syntax/Extensions/TokenSyntax.swift | 11 +- .../Extensions/TypeSyntaxProtocol.swift | 35 +++-- .../SerdeDefault/Syntax/FunctionDecl.swift | 14 +- .../SerdeDefault/Syntax/PatternBinding.swift | 9 +- .../SerdeDefault/Syntax/VariableDecl.swift | 10 +- .../SerdeDefaultMacro.swift | 8 +- .../SerdeMacro.swift | 2 +- .../SerdeDefaultMacroTests.swift | 138 ++++++++++++++---- 27 files changed, 357 insertions(+), 206 deletions(-) diff --git a/Sources/DefaultsMacros/SerdeDefault.swift b/Sources/DefaultsMacros/SerdeDefault.swift index 4dd59aff..f6110e31 100644 --- a/Sources/DefaultsMacros/SerdeDefault.swift +++ b/Sources/DefaultsMacros/SerdeDefault.swift @@ -1,58 +1,62 @@ import Defaults /** -Supported arguments for `@serde`. +Options for customizing how a property is serialized. */ public enum SerdeOption { + /** + Exclude the property from serialization and deserialization. + */ case skip } /** -Attached macro that synthesizes `Defaults.Serializable` conformance for a struct. +Make a struct conform to `Defaults.Serializable`. -This macro generates: - -1. A nested `Bridge` type named `Bridge`. -2. `serialize(_:)` and `deserialize(_:)` implementations based on stored properties. -3. A static `bridge` instance used by `Defaults`. - -For example, given: +The macro uses the struct's stored properties to serialize and deserialize it. ```swift +import Defaults +import DefaultsMacros + @SerdeDefault struct User { var name: String var age: UInt? } -``` - -The macro generates an extension equivalent to: -```swift -extension User: Defaults.Serializable { - struct UserBridge: Defaults.Bridge { - typealias Value = User - typealias Serializable = [String: Any] - // serialize(_:) and deserialize(_:) - } - - static let bridge = UserBridge() +extension Defaults.Keys { + static let user = Key( + "user", + default: User(name: "Appleseed", age: nil) + ) } + +Defaults[.user].name +//=> "Appleseed" ``` -- Important: In this stage, `@SerdeDefault` supports `struct` declarations only. +- Important: This macro only supports non-generic structs. */ @attached(extension, conformances: Defaults.Serializable, names: named(bridge), suffixed(Bridge)) public macro SerdeDefault() = #externalMacro(module: "DefaultsMacrosDeclarations", type: "SerdeDefaultMacro") /** -Peer macro used on stored properties in `@SerdeDefault` structs to configure serde behavior. +Customize how a stored property in a `@SerdeDefault` struct is serialized. -Supported arguments: +Use `.skip` to exclude an optional property from serialization and deserialization. -1. `@serde(.skip)`: Excludes the property from serialization and deserialization. +```swift +@SerdeDefault +struct User { + var name: String + + @serde(.skip) + var cachedAvatar: Data? +} +``` -- Important: `skip` is only valid on optional properties. +- Important: Only optional properties can use `.skip`. */ @attached(peer) public macro serde(_ options: SerdeOption...) = #externalMacro( diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift index a9522e3a..e0e30ad9 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Deserialize.swift @@ -2,13 +2,14 @@ import SwiftSyntax extension SerdeDefault { /** - Code generator for func deserialize code blocks. + Code generator for the `deserialize` function body. For native properties: ```swift let property = base["property"] as? PropertyType ``` + For serializable properties: ```swift @@ -21,6 +22,7 @@ extension SerdeDefault { The serialized dictionary parameter identifier being deserialized. */ let base: TokenSyntax + /** The nominal type being rebuilt. */ @@ -63,8 +65,6 @@ extension SerdeDefault { /** The initializer expression for the nominal type, using the generated arguments. - - */ var initializer: FunctionCallExprSyntax { let callee: ExprSyntax = "\(typeSyntax.trimmed)" diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift index 83cfd6b6..d5c8a05f 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeBlock/Serialize.swift @@ -3,7 +3,7 @@ import SwiftSyntaxMacros extension SerdeDefault { /** - Code generator for func serialize code blocks. + Code generator for the `serialize` function body. For native properties: @@ -24,23 +24,21 @@ extension SerdeDefault { The value parameter identifier being serialized. */ let base: TokenSyntax + /** The dictionary identifier that receives serialized values. */ let serialized: TokenSyntax + /** The expansion context, used to mint collision-free identifiers. */ let macroContext: any MacroExpansionContext /** - Returns a collision-free name for the temporary that holds `identifier`'s value. + Get a collision-free name for the temporary that holds `identifier`'s value. - `base` and `serialized` are the only other identifiers declared in the generated - function, and property names reach it only as member accesses (`value.name`) or - dictionary keys. Minting the temporaries is therefore enough to keep the whole - function hygienic — a property named `serialized` would otherwise shadow the - dictionary it is being written into. + `base` and `serialized` are the only other identifiers declared in the generated function, and property names reach it only as member accesses (`value.name`) or dictionary keys. Minting the temporaries is therefore enough to keep the whole function hygienic — a property named `serialized` would otherwise shadow the dictionary it is being written into. */ func makeUniqueName(for identifier: TokenSyntax) -> TokenSyntax { macroContext.makeUniqueName(identifier.text) @@ -101,8 +99,7 @@ extension SerdeDefault { property.serialize(in: context) } let codeGenerated = SerializedCodeGenerated(codesGenerated: codesGenerated) - // Only bind with `var` when something actually mutates the dictionary, otherwise - // every expansion warns that it was never mutated. + // Only bind with `var` when something actually mutates the dictionary, otherwise every expansion warns that it was never mutated. let bindingSpecifier: TokenSyntax = codeGenerated.assignments.isEmpty ? .keyword(.let) : .keyword(.var) diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift index 5636c31e..1c1eeff4 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/CodeGenerating/CodeGenerating.swift @@ -9,7 +9,7 @@ extension SerdeDefault { associatedtype CodeGenerated /** - Generates a code block for multiple properties from their generated code. + Generate a code block for multiple properties from their generated code. */ static func codeGeneratedBlock( from properties: [any Property], diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift index 47392783..b0e5274e 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl+Bridge.swift @@ -16,10 +16,9 @@ extension SerdeDefault.NominalDecl { } /** - Generates the stored bridge instance for the expanded extension. + Generate the stored bridge instance for the expanded extension. - The generated member is `static let bridge = ()`. If the source - nominal type is wider than `internal`, that access level is preserved. + The generated member is `static let bridge = ()`. If the source nominal type is wider than `internal`, that access level is preserved. - Returns: The generated `bridge` declaration. */ @@ -29,7 +28,8 @@ extension SerdeDefault.NominalDecl { pattern: IdentifierPatternSyntax(identifier: IdentifierTypeSyntax.bridge.name), initializer: InitializerClauseSyntax( value: FunctionCallExprSyntax(callee: bridgeName.declReference()) - )) + ) + ) return VariableDeclSyntax( modifiers: DeclModifierListSyntax(modifiers), bindingSpecifier: .keyword(.let), @@ -38,7 +38,7 @@ extension SerdeDefault.NominalDecl { } /** - Generates the nested `Defaults.Bridge` implementation. + Generate the nested `Defaults.Bridge` implementation. - Returns: The generated bridge type declaration. */ @@ -102,18 +102,18 @@ extension SerdeDefault.NominalDecl { return StructDeclSyntax( modifiers: modifiers, name: bridgeName, - inheritanceClause: InheritanceClauseSyntax( - inheritedTypes: InheritedTypeListSyntax([ - InheritedTypeSyntax( - type: MemberTypeSyntax.DefaultsBridge, - trailingComma: .commaToken() - ), - InheritedTypeSyntax( - type: IdentifierTypeSyntax.Sendable - ) - ]) - ), - memberBlock: memberBlock - ) + inheritanceClause: InheritanceClauseSyntax( + inheritedTypes: InheritedTypeListSyntax([ + InheritedTypeSyntax( + type: MemberTypeSyntax.DefaultsBridge, + trailingComma: .commaToken() + ), + InheritedTypeSyntax( + type: IdentifierTypeSyntax.Sendable + ) + ]) + ), + memberBlock: memberBlock + ) } } diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift index b4627f48..65329d46 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/NominalDecl.swift @@ -10,24 +10,29 @@ extension SerdeDefault { The nominal type name. */ let identifier: TokenSyntax + /** The access level of the nominal type. If the declaration does not specify an access level, this defaults to `internal`. */ let accessLevel: AccessLevel + /** The actual type syntax for the nominal type. */ let typeSyntax: TypeSyntax + /** The properties of the nominal type. */ let properties: Properties + /** The context for code generation of the `serialize` function. */ let serializeContext: SerdeDefault.Serialize.Context + /** The context for code generation of the `deserialize` function. */ @@ -50,27 +55,22 @@ extension SerdeDefault { type: some TypeSyntaxProtocol, context: MacroExpansionContext ) throws(SerdeDefault.Error) { - // TODO: Support generic types. This needs the generated extension to carry the - // right `where` clauses, so reject it outright instead of expanding to code that - // fails with an unrelated error. + // TODO: Support generic types. This needs the generated extension to carry the right `where` clauses, so reject it outright instead of expanding to code that fails with an unrelated error. if let genericParameterClause = structDecl.genericParameterClause { throw SerdeDefault.Error.genericType(genericParameterClause) } identifier = structDecl.name accessLevel = AccessLevel(modifiers: structDecl.modifiers) - let typeSyntax = TypeSyntax(fromProtocol: type) - self.typeSyntax = typeSyntax + typeSyntax = TypeSyntax(fromProtocol: type) let variables = structDecl.memberBlock.members .compactMap { SerdeDefault.VariableDecl(declaration: $0.decl) - // Ignore static properties, as they cannot be serialized or deserialized. } + // Ignore static properties, as they cannot be serialized or deserialized. .filter { !$0.isStatic() } let bindings = try Self.bindings(variables: variables) properties = try Properties(bindings: bindings) - // `base` and `serialized` can be spelled literally because property names reach the - // generated function only as member accesses (`value.name`) or dictionary keys. The - // temporaries the properties bind are the part that has to be unique. + // `base` and `serialized` can be spelled literally because property names reach the generated function only as member accesses (`value.name`) or dictionary keys. The temporaries the properties bind are the part that has to be unique. serializeContext = SerdeDefault.Serialize.Context( base: "value", serialized: IdentifierTypeSyntax.serialized.name, @@ -83,7 +83,7 @@ extension SerdeDefault { } /** - Extracts stored bindings from the nominal type variables. + Extract stored bindings from the nominal type variables. */ static func bindings(variables: [VariableDecl]) throws(SerdeDefault.Error) -> [PatternBinding] { try variables diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift index d64f4a38..7db1d2e2 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift @@ -5,7 +5,7 @@ extension SerdeDefault { let properties: [any Property] /** - Initializes properties from the nominal type bindings. + Create properties from the nominal type bindings. - Parameter bindings: Pattern bindings from the nominal type. */ @@ -16,10 +16,10 @@ extension SerdeDefault { } /** - Generates a code block for these properties using the given generator. + Generate a code block for these properties using the given generator. - Parameter generator: The property-code generator to use. - - Parameter context: Context passed through to the generator. + - Parameter context: The context passed to the generator. */ func codeGenerated( using generator: Generator.Type, diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift index c7164149..c175b472 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/BasicProperty.swift @@ -64,11 +64,13 @@ extension SerdeDefault { let type = unwrappedTypeSyntax let value: ExprSyntax = "\(context.base)[\(literal: identifier.text)] as? \(type)" - // A default value wins over optionality, so an optional property with an initializer - // falls back to that initializer rather than to `nil`. + // A default value wins over optionality, so an optional property with an initializer falls back to that initializer rather than to `nil`. let statement: CodeBlockItemSyntax if let initializer = binding.syntax.initializer { - statement = "let \(identifier) = \(value) ?? \(initializer.value.trimmed)" + let defaultValue = initializer.value.replacingSelfReferences( + with: IdentifierTypeSyntax.Value.name + ) + statement = "let \(identifier) = \(value) ?? \(defaultValue.trimmed)" } else if typeSyntax.isOptional() { statement = "let \(identifier) = \(value)" } else { diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift index abe39453..071ca9d1 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/Property.swift @@ -4,7 +4,9 @@ extension SerdeDefault { /** Stored property prepared for bridge generation. - Conformers own the serialization and deserialization code generation for one kind of property, and know whether they apply to a given binding via `matches(binding:attribute:)`. Structure shared by every kind — skip and omission checks, optional handling, `guard` scaffolding — lives in the protocol extension below so conformers only provide what actually varies. + Types that conform to this protocol generate the serialization and deserialization code for one kind of property. + + The type should implement `matches(binding:attribute:)` to declare which bindings it applies to. */ protocol Property { /** @@ -17,23 +19,29 @@ extension SerdeDefault { */ var attribute: SerdeAttribute { get } + /** + Create a property for a binding that `matches(binding:attribute:)` has accepted. + + - Parameter binding: The pattern binding to generate code for. + - Parameter attribute: The parsed `@serde` arguments for `binding`. + */ init(binding: PatternBinding, attribute: SerdeAttribute) /** - Returns whether this property kind applies to `binding`. + Check whether this property kind applies to `binding`. - - Parameter attribute: Parsed `@serde` arguments for `binding`, so a kind selected by an - attribute rather than by type can recognize itself. + - Parameter binding: The pattern binding to inspect. + - Parameter attribute: The parsed `@serde` arguments for `binding`. */ static func matches(binding: PatternBinding, attribute: SerdeAttribute) -> Bool /** - Generates the serialization code contributed by this property. + Generate the serialization code contributed by this property. */ func serialize(in context: Serialize.Context) -> Serialize.CodeGenerated /** - Generates the deserialization code contributed by this property. + Generate the deserialization code contributed by this property. */ func deserialize(in context: Deserialize.Context) -> Deserialize.CodeGenerated } @@ -52,9 +60,6 @@ extension SerdeDefault.Property { /** Property type with any optional wrapper removed. - - Trimmed, since the type is interpolated into generated statements where leftover - trivia from the source (for example, `Int ` in `var a: Int = 1`) would leak through. */ var unwrappedTypeSyntax: TypeSyntax { guard let wrappedType = typeSyntax.optionalWrappedType() else { @@ -64,9 +69,14 @@ extension SerdeDefault.Property { } /** - Returns whether this property should be excluded from serialization and deserialization. + Check whether this property should be excluded from serialization and deserialization. + + For properties: + + - marked with `@serde(.skip)` + - that are constants with default values - Properties marked with `@serde(.skip)` or constants with default values are excluded, as they cannot be reliably deserialized. + cannot be serialized or deserialized, so they are excluded from the generated code. */ func isExcludedFromSerde() -> Bool { attribute.skip || !binding.isMemberwiseInitializable() diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift index 5d63dc00..419b6178 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/SerializableProperty.swift @@ -4,8 +4,7 @@ extension SerdeDefault { /** Property that needs `Defaults.Serializable` conversion through its bridge. - This is the catch-all strategy: `resolve(binding:)` always succeeds, so it must stay - registered last in `StrategyFinder`. + This is the catch-all strategy: `resolve(binding:)` always succeeds, so it must stay registered last in `StrategyFinder`. For serialization: @@ -66,11 +65,13 @@ extension SerdeDefault { let value: ExprSyntax = "\(type).toValue(\(context.base)[\(literal: identifier.text)] as Any, type: \(type).self)" - // A default value wins over optionality, so an optional property with an initializer - // falls back to that initializer rather than to `nil`. + // A default value wins over optionality, so an optional property with an initializer falls back to that initializer rather than to `nil`. let statement: CodeBlockItemSyntax if let initializer = binding.syntax.initializer { - statement = "let \(identifier) = \(value) ?? \(initializer.value.trimmed)" + let defaultValue = initializer.value.replacingSelfReferences( + with: IdentifierTypeSyntax.Value.name + ) + statement = "let \(identifier) = \(value) ?? \(defaultValue.trimmed)" } else if typeSyntax.isOptional() { statement = "let \(identifier) = \(value)" } else { diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift index f8d3644a..7bae845f 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties/StrategyFinder.swift @@ -2,7 +2,7 @@ import SwiftSyntax extension SerdeDefault { /** - Resolves the `Property` implementation for a binding by trying registered strategies in order. + Resolver that picks the `Property` implementation for a binding by trying registered strategies in order. */ struct StrategyFinder { /** @@ -18,14 +18,11 @@ extension SerdeDefault { } /** - Resolves the `Property` implementation for `binding`. + Resolve the `Property` implementation for `binding`. - The binding's `@serde` arguments are parsed once up front and handed to every candidate, so - strategies selected by an attribute can recognize themselves without re-parsing. + The binding's `@serde` arguments are parsed once up front and handed to every candidate, so strategies selected by an attribute can recognize themselves without re-parsing. - - Throws: `Error` if the `@serde` arguments are invalid, or `Error.unresolvedProperty` if no - registered strategy matches the binding. The latter should be unreachable while a catch-all - strategy is registered. + - Throws: `Error` if the `@serde` arguments are invalid, or `Error.unresolvedProperty` if no registered strategy matches the binding. */ static func resolve(binding: PatternBinding) throws(Error) -> any Property { let attribute = try SerdeAttribute(binding: binding) @@ -34,25 +31,5 @@ extension SerdeDefault { } throw Error.unresolvedProperty(binding.syntax) } - - /** - Re-resolves `property` to the one strategy among `strategies` that matches it. - - Reuses the already-parsed binding and attribute, so — unlike `resolve(binding:attribute:)` — - this cannot fail. `property` is returned unchanged when nothing matches. - - - Parameter strategies: Strategies to try, in order. A strategy that wraps the result has to - narrow this to the strategies after itself, or it matches its own binding and recurses. - */ - static func resolve( - property: any Property, - _ strategies: [any Property.Type] = strategies - ) -> any Property { - for strategy in strategies - where strategy.matches(binding: property.binding, attribute: property.attribute) { - return strategy.init(binding: property.binding, attribute: property.attribute) - } - return property - } } } diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift index 9046ff18..23c66b2a 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/SerdeAttribute.swift @@ -6,7 +6,7 @@ extension SerdeDefault { */ struct SerdeAttribute { /** - Name of the `@serde` attribute + Name of the `@serde` attribute. */ static let name = IdentifierTypeSyntax.serde.name @@ -16,7 +16,7 @@ extension SerdeDefault { let skip: Bool /** - Parses and validates `@serde` arguments for a property binding. + Parse and validate `@serde` arguments for a property binding. */ init(binding: PatternBinding) throws(Error) { try self.init(attributes: binding.attributes) @@ -26,11 +26,10 @@ extension SerdeDefault { } /** - Parses `@serde` arguments from an attribute list. + Parse `@serde` arguments from an attribute list. - Parameter attributes: Attributes to inspect. - - Throws: `Error.mismatch` for invalid argument syntax, or - `Error.unknownAttribute` for unsupported arguments. + - Throws: `Error.mismatch` for invalid argument syntax, or `Error.unknownAttribute` for unsupported arguments. - Returns: Parsed attribute values. */ init(attributes: AttributeListSyntax) throws(Error) { @@ -49,7 +48,8 @@ extension SerdeDefault { // Parse arguments only from `@serde(...)` and `@DefaultsMacros.serde(...)`. guard attribute.attributeName.isEqual( - to: Self.name, inModule: IdentifierTypeSyntax.DefaultsMacros.name) + to: Self.name, inModule: IdentifierTypeSyntax.DefaultsMacros.name + ) else { continue } @@ -68,7 +68,8 @@ extension SerdeDefault { } /** - Finds the attribute name from a labeled expression. + Find the attribute name from a labeled expression. + Accepts `@serde(.skip)` and explicitly qualified `SerdeOption.skip` spellings. */ static func findAttributeName(argument: LabeledExprSyntax) throws(Error) -> TokenSyntax { diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift index bc0bc9fa..fcb0bda4 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/SerdeDefault+Error.swift @@ -10,30 +10,37 @@ extension SerdeDefault { The declaration is not a supported nominal type (`struct` only). */ case unexpectedNominalType(any SyntaxProtocol) + /** The nominal declaration has generic parameters. */ case genericType(any SyntaxProtocol) + /** The syntax node does not match the expected type. */ case mismatch(Any.Type, any SyntaxProtocol) + /** The property binding is missing a type annotation. */ case missingType(any SyntaxProtocol) + /** The stored property type could not be inferred from its initializer. */ case inferredType(any SyntaxProtocol) + /** A non-optional property is marked with `@serde(.skip)`, which is unsupported. */ case skipOnNonOptional(any SyntaxProtocol) + /** An attribute is not recognized. */ case unknownAttribute(any SyntaxProtocol, String) + /** No registered `Property` strategy could resolve the binding. @@ -41,6 +48,11 @@ extension SerdeDefault { */ case unresolvedProperty(any SyntaxProtocol) + /** + Get the syntax node where the diagnostic should be displayed. + + Every error retains the node that caused it, allowing callers to attach the diagnostic to the most relevant source location. + */ func syntax() -> any SyntaxProtocol { switch self { case .unexpectedNominalType(let got): @@ -62,6 +74,9 @@ extension SerdeDefault { } } + /** + A stable identifier derived from the error case. + */ var caseName: String { switch self { case .unexpectedNominalType: diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift index 78233d0b..66a953e0 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/IdentifierTypeSyntax.swift @@ -13,6 +13,8 @@ extension SwiftSyntax.IdentifierTypeSyntax { // MARK: Identifiers for Swift and Foundation types static let Swift = Self(name: "Swift") + static let Foundation = Self(name: "Foundation") + static let CoreGraphics = Self(name: "CoreGraphics") static let `Optional` = Self(name: "Optional") static let `Any` = Self(name: "Any") static let Sendable = Self(name: "Sendable") @@ -38,14 +40,17 @@ extension SwiftSyntax.IdentifierTypeSyntax { Identifier for the `@serde` attribute. */ static let serde = Self(name: "serde") + /** Identifier for the `DefaultsMacros` module used to qualify `SerdeOption`. */ static let DefaultsMacros = Self(name: "DefaultsMacros") + /** Identifier for the `SerdeOption` enum used by `@serde`. */ static let SerdeOption = Self(name: "SerdeOption") + /** Identifier for the `skip` argument used in `@serde(.skip)`. */ diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift index 20dcfc91..b18ac2c0 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Constants/MemberTypeSyntax.swift @@ -7,12 +7,14 @@ extension SwiftSyntax.MemberTypeSyntax { static let DefaultsMacrosSerdeOption = Self( baseType: SwiftSyntax.IdentifierTypeSyntax.DefaultsMacros, name: SwiftSyntax.IdentifierTypeSyntax.SerdeOption.name) + /** Type syntax for `Defaults.Bridge`. */ static let DefaultsBridge = Self( baseType: SwiftSyntax.IdentifierTypeSyntax.Defaults, name: SwiftSyntax.IdentifierTypeSyntax.Bridge.name) + /** Type syntax for `Defaults.Serializable`. */ diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift index f957aa00..3cf7877f 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/DeclModifierSyntax.swift @@ -5,6 +5,7 @@ extension SwiftSyntax.DeclModifierSyntax { Reusable `public` modifier for generated declarations. */ static let `public` = Self(name: .keyword(.public)) + /** Reusable `static` modifier for generated declarations. */ diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift index 40bc2a4d..53f7566e 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/ExprSyntax.swift @@ -1,8 +1,14 @@ import SwiftSyntax extension ExprSyntax { + func replacingSelfReferences(with replacement: TokenSyntax) -> ExprSyntax { + SelfReferenceRewriter(replacement: replacement) + .rewrite(self) + .cast(ExprSyntax.self) + } + /** - Infers a Swift type from simple literals and type expressions. + Infer a Swift type from simple literals and type expressions. */ func inferredType() -> (any TypeSyntaxProtocol)? { switch self.as(ExprSyntaxEnum.self) { @@ -15,6 +21,7 @@ extension ExprSyntax { } case .tupleExpr(let tupleExpr): + // only infer a type from a single-element tuple, since multi-element tuples are not supported in `Defaults`. guard tupleExpr.elements.count == 1, let element = tupleExpr.elements.first, @@ -25,25 +32,13 @@ extension ExprSyntax { return element.expression.inferredType() - case .booleanLiteralExpr: - return IdentifierTypeSyntax.Bool - - case .integerLiteralExpr: - return IdentifierTypeSyntax.Int - - case .floatLiteralExpr: - return IdentifierTypeSyntax.Double - - case .stringLiteralExpr: - return IdentifierTypeSyntax.String - - case .typeExpr(let typeExpr): - return typeExpr.type - case .arrayExpr(let arrayExpr): guard let firstElement = arrayExpr.elements.first, - let type = firstElement.expression.inferredType() + let type = firstElement.expression.inferredType(), + arrayExpr.elements.dropFirst().allSatisfy({ + $0.expression.inferredType()?.trimmedDescription == type.trimmedDescription + }) else { return nil } @@ -55,7 +50,11 @@ extension ExprSyntax { case .elements(let elements) = dictionaryExpr.content, let firstElement = elements.first, let keyType = firstElement.key.inferredType(), - let valueType = firstElement.value.inferredType() + let valueType = firstElement.value.inferredType(), + elements.dropFirst().allSatisfy({ + $0.key.inferredType()?.trimmedDescription == keyType.trimmedDescription + && $0.value.inferredType()?.trimmedDescription == valueType.trimmedDescription + }) else { return nil } @@ -65,8 +64,39 @@ extension ExprSyntax { value: TypeSyntax(fromProtocol: valueType) ) + case .typeExpr(let typeExpr): + return typeExpr.type + + case .booleanLiteralExpr: + return IdentifierTypeSyntax.Bool + + case .integerLiteralExpr: + return IdentifierTypeSyntax.Int + + case .floatLiteralExpr: + return IdentifierTypeSyntax.Double + + case .stringLiteralExpr: + return IdentifierTypeSyntax.String + default: return nil } } } + +private final class SelfReferenceRewriter: SyntaxRewriter { + private let replacement: TokenSyntax + + init(replacement: TokenSyntax) { + self.replacement = replacement + } + + override func visit(_ token: TokenSyntax) -> TokenSyntax { + guard token.tokenKind == .keyword(.Self) else { + return token + } + + return token.with(\.tokenKind, .identifier(replacement.text)) + } +} diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift index 1180a83a..5ff04e0c 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/OptionalType.swift @@ -2,11 +2,11 @@ import SwiftSyntax extension SerdeDefault { /** - Supports wrapping syntax nodes in `OptionalTypeSyntax`. + Support for wrapping syntax nodes in `OptionalTypeSyntax`. */ protocol OptionalType { /** - Wraps the type in `OptionalTypeSyntax`. + Wrap the type in `OptionalTypeSyntax`. */ func optional() -> OptionalTypeSyntax } diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift index 542d9b9a..ceafd4f4 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/SyntaxProtocol.swift @@ -2,12 +2,11 @@ import SwiftSyntax extension SyntaxProtocol { /** - Returns whether the syntax is equal to the given name and module. + Check whether the syntax is equal to the given name and module. - Parameter name: The name to compare against. - Parameter module: The module to compare against. - - Returns: `true` if the syntax is equal to the name and module, - `false` otherwise. + - Returns: `true` if the syntax is equal to the name and module, `false` otherwise. */ func isEqual(to name: TokenSyntax, inModule module: TokenSyntax) -> Bool { switch Syntax(self).as(SyntaxEnum.self) { diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift index 6db713e4..2a5a8d20 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TokenSyntax.swift @@ -2,7 +2,7 @@ import SwiftSyntax extension TokenSyntax { /** - Creates a declaration-reference expression from the token text. + Create a declaration-reference expression from the token text. */ func declReference() -> DeclReferenceExprSyntax { DeclReferenceExprSyntax(leadingTrivia: nil, baseName: self.trimmed, trailingTrivia: nil) @@ -13,11 +13,16 @@ extension TokenSyntax { } /** - Returns a token with `suffix` appended. + Get a token with `suffix` appended. - Parameter suffix: The suffix to append. */ func suffix(_ suffix: String) -> Self { - TokenSyntax(stringLiteral: "\(self.text)\(suffix)") + let base = if text.hasPrefix("`") && text.hasSuffix("`") { + String(text.dropFirst().dropLast()) + } else { + text + } + return .identifier("\(base)\(suffix)") } } diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift index dcd40ffb..915a5146 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/Extensions/TypeSyntaxProtocol.swift @@ -2,7 +2,7 @@ import SwiftSyntax extension TypeSyntaxProtocol { /** - Returns generic arguments from either: + Get the generic arguments from either: - `IdentifierTypeSyntax` (for example, `Optional`) - `MemberTypeSyntax` (for example, `Swift.Optional`) @@ -26,12 +26,12 @@ extension TypeSyntaxProtocol { } /** - Returns the wrapped type for any supported optional spelling: + Get the wrapped type for any supported optional spelling: - `T?` - `Optional` - `Swift.Optional` -cv + Generated `toValue` calls use this to convert to `T` instead of producing nested optionals. */ func optionalWrappedType() -> (any TypeSyntaxProtocol)? { @@ -51,10 +51,9 @@ cv } /** - Returns whether the type is exactly `Swift.Optional`. + Check whether the type is exactly `Swift.Optional`. - This explicit check avoids accidentally matching unrelated `Optional` symbols - from other modules or nested types. + This explicit check avoids accidentally matching unrelated `Optional` symbols from other modules or nested types. */ func isSwiftMemberGenericOptional() -> Bool { if let memberType = self.as(MemberTypeSyntax.self) { @@ -67,7 +66,7 @@ cv } /** - Returns whether the type is unqualified `Optional`. + Check whether the type is unqualified `Optional`. */ func isGenericOptional() -> Bool { if let identifierType = self.as(IdentifierTypeSyntax.self) { @@ -79,7 +78,7 @@ cv } /** - Returns whether the type is any supported optional spelling. + Check whether the type is any supported optional spelling. */ func isOptional() -> Bool { self.is(OptionalTypeSyntax.self) || self.isGenericOptional() @@ -87,7 +86,7 @@ cv } /** - Returns whether `Defaults` can store this type without a bridge. + Check whether `Defaults` can store this type without a bridge. */ func isNativeSupportType() -> Bool { if let wrappedType = optionalWrappedType() { @@ -106,7 +105,23 @@ cv } return dictionaryType.value.isNativeSupportType() case .memberType(let memberType): - return IdentifierTypeSyntax(name: memberType.name).isNativeSupportType() + guard + let baseType = memberType.baseType.as(IdentifierTypeSyntax.self), + IdentifierTypeSyntax.nativeSupportedTypes.contains(where: { + $0.name.text == memberType.name.text + }) + else { + return false + } + + switch memberType.name.text { + case IdentifierTypeSyntax.Data.name.text, IdentifierTypeSyntax.Date.name.text: + return baseType.name.text == IdentifierTypeSyntax.Foundation.name.text + case IdentifierTypeSyntax.CGFloat.name.text: + return baseType.name.text == IdentifierTypeSyntax.CoreGraphics.name.text + default: + return baseType.name.text == IdentifierTypeSyntax.Swift.name.text + } case .identifierType(let identifierType): return IdentifierTypeSyntax.nativeSupportedTypes.contains { $0.name.text == identifierType.name.text diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift index 358ebebb..17f33992 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift @@ -9,10 +9,12 @@ extension SerdeDefault { The generated function name. */ let name: TokenSyntax + /** The generated function access level. */ var accessLevel: AccessLevel = .internal + /** Parameters collected for the function signature. */ @@ -30,11 +32,11 @@ extension SerdeDefault { } /** - Adds a parameter to the function signature. + Add a parameter to the function signature. - - Parameter firstName: External parameter name. - - Parameter secondName: Internal parameter name. - - Parameter type: Parameter type. + - Parameter firstName: The external parameter name. + - Parameter secondName: The internal parameter name. + - Parameter type: The parameter type. */ func addParameter( firstName: TokenSyntax = .wildcardToken(), @@ -51,7 +53,7 @@ extension SerdeDefault { } /** - Sets the return type for the function signature. + Set the return type for the function signature. - Parameter returnType: The return type syntax. */ @@ -61,7 +63,7 @@ extension SerdeDefault { } /** - Builds the final function declaration. + Build the final function declaration. - Returns: The generated function declaration. */ diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift index 2cf42a96..114c8c3f 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/PatternBinding.swift @@ -22,7 +22,7 @@ extension SerdeDefault { } /** - Extracts the identifier from a pattern binding. + Extract the identifier from a pattern binding. - Throws: `SerdeDefault.Error.mismatch` when the pattern is not an identifier. */ @@ -35,7 +35,7 @@ extension SerdeDefault { } /** - Returns whether this binding can be initialized with the memberwise initializer. + Check whether this binding can be initialized with the memberwise initializer. */ func isMemberwiseInitializable() -> Bool { isStored() @@ -43,10 +43,9 @@ extension SerdeDefault { } /** - Returns whether this binding represents a stored property. + Check whether this binding represents a stored property. - Computed properties with a getter are excluded. - Observed properties (`willSet`/`didSet`) are treated as stored. + Computed properties with a getter are excluded. Observed properties (`willSet`/`didSet`) are treated as stored. */ func isStored() -> Bool { // Bindings without an accessor block are stored properties. diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift index 4fe83004..f549a842 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/VariableDecl.swift @@ -12,7 +12,7 @@ extension SerdeDefault { } /** - Returns whether the variable declaration is static. + Check whether the variable declaration is static. */ func isStatic() -> Bool { syntax.modifiers.contains { modifier in @@ -21,7 +21,7 @@ extension SerdeDefault { } /** - Extracts typed bindings from the variable declaration. + Extract typed bindings from the variable declaration. */ func typedBindings() throws(Error) -> [PatternBinding] { let specifier = self.syntax.bindingSpecifier @@ -36,11 +36,9 @@ extension SerdeDefault { } /** - Resolves the type for the binding at `index`. + Resolve the type for the binding at `index`. - If the binding has an initializer, simple literal types are inferred from it. - Otherwise, this falls back to the next explicit type in the same declaration - (for example, `var a, b: Int`). + If the binding has an initializer, simple literal types are inferred from it. Otherwise, this falls back to the next explicit type in the same declaration (for example, `var a, b: Int`). */ private func type(for binding: PatternBindingSyntax, at index: Int) throws(Error) -> TypeSyntax { if let typeSyntax = binding.typeAnnotation?.type { diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift index 934a4ee8..22fc1636 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefaultMacro.swift @@ -3,6 +3,9 @@ import SwiftSyntax import SwiftSyntaxMacros /// Macro declaration for the ``SerdeDefault`` macro. +/** +The implementation of the `@SerdeDefault` macro. +*/ public struct SerdeDefaultMacro {} enum SerdeDefault {} @@ -11,8 +14,9 @@ extension SerdeDefaultMacro: ExtensionMacro { /** Generates the extension declaration that adds `Defaults.Serializable` conformance. - - Parameter declaration: The declaration annotated with `@SerdeDefault`. - - Parameter type: The concrete type being extended. + - Parameters: + - declaration: The declaration annotated with `@SerdeDefault`. + - type: The concrete type being extended. - Returns: The generated extension declaration. */ diff --git a/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift b/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift index d69430be..4e966683 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeMacro.swift @@ -2,7 +2,7 @@ import SwiftSyntax import SwiftSyntaxMacros /** -Macro declaration for the ``serde`` macro. +The implementation of the `@serde` macro. */ public struct SerdeMacro {} diff --git a/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift b/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift index 1e9bfd9d..bb0f240a 100644 --- a/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift +++ b/Tests/DefaultsMacrosDeclarationsTests/SerdeDefaultMacroTests.swift @@ -1,3 +1,4 @@ +import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest @@ -24,14 +25,6 @@ import XCTest case guest = "Guest" } - // @SerdeDefault - // private struct User { - // let name: String - // let age: UInt? - // let role: Role? - // @serde(.skip) - // let group: Group? - // } #else private let testMacros: [String: Macro.Type] = [:] #endif @@ -334,6 +327,49 @@ final class SerdeDefaultMacroTests: XCTestCase { ) } + func testExpansionQualifiesSelfInDefaultValuesAsValue() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Settings { + static let fallback = 1 + var count: Int = Self.fallback + } + """#, + expandedSource: #""" + struct Settings { + static let fallback = 1 + var count: Int = Self.fallback + } + + extension Settings: Defaults.Serializable { + struct SettingsBridge: Defaults.Bridge, Sendable { + typealias Value = Settings + typealias Serializable = [String: Any] + public func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + let serialized: Serializable = ["count": value.count] + + return serialized + } + public func deserialize(_ __macro_local_10serializedfMu_: Serializable?) -> Value? { + guard let __macro_local_10serializedfMu_ else { + return nil + } + let count = __macro_local_10serializedfMu_["count"] as? Int ?? Value.fallback + return Settings(count: count) + } + } + static let bridge = SettingsBridge() + } + """#, + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + func testExpansionInfersWrappedAndSignedLiteralMemberTypes() throws { assertMacroExpansion( #""" @@ -668,10 +704,73 @@ final class SerdeDefaultMacroTests: XCTestCase { ) } - /** - A property named after one of the identifiers the generated `serialize` declares would - shadow it, so the temporaries it binds have to be unique names. - */ + func testExpansionDiagnosesClass() throws { + assertMacroExpansion( + #""" + @SerdeDefault + class User { + var name: String + } + """#, + expandedSource: #""" + class User { + var name: String + } + """#, + diagnostics: [ + DiagnosticSpec( + message: "Expected a struct declaration. Received: ClassDeclSyntax. In this first-stage implementation, `@SerdeDefault` supports structs only.", + line: 1, + column: 1 + ) + ], + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testExpansionDiagnosesMixedCollectionLiteralTypes() throws { + assertMacroExpansion( + #""" + @SerdeDefault + struct Values { + var numbers = [1, 2.5] + } + """#, + expandedSource: #""" + struct Values { + var numbers = [1, 2.5] + } + """#, + diagnostics: [ + DiagnosticSpec( + message: "Could not infer the type of `@SerdeDefault` stored property initializer. Add an explicit type annotation.", + line: 3, + column: 16 + ) + ], + macros: testMacros, + indentationWidth: .tabs(1) + ) + } + + func testNativeTypeResolution() { + let nativeType: TypeSyntax = "String" + XCTAssertTrue(nativeType.isNativeSupportType()) + + let lookalikeType: TypeSyntax = "Domain.String" + XCTAssertFalse(lookalikeType.isNativeSupportType()) + + let qualifiedType: TypeSyntax = "Swift.String" + XCTAssertTrue(qualifiedType.isNativeSupportType()) + } + + func testSuffixRemovesIdentifierEscaping() { + let identifier = TokenSyntax(stringLiteral: "`actor`") + XCTAssertEqual(identifier.suffix("Bridge").text, "actorBridge") + } + + // Generated temporaries must not shadow properties with the same name. func testExpansionKeepsSerializeHygienicForCollidingPropertyNames() throws { assertMacroExpansion( #""" @@ -766,19 +865,4 @@ final class SerdeDefaultMacroTests: XCTestCase { indentationWidth: .tabs(1) ) } - - // func testDefaultsKeyUsage() throws { - // #if canImport(DefaultsMacrosDeclarations) - // let defaultUser = User(name: "Alice", age: 30, role: .admin, group: .standalone) - // let key = Defaults.Key("serdeDefaultsKeyUsage", default: defaultUser, suite: suite) - // XCTAssertEqual(Defaults[key].name, defaultUser.name) - // let newUser = User(name: "Bob", age: nil, role: .guest, group: .standalone) - // Defaults[key] = newUser - // XCTAssertEqual(Defaults[key].name, newUser.name) - // XCTAssertEqual(Defaults[key].age, newUser.age) - // XCTAssertEqual(Defaults[key].role, newUser.role) - // #else - // throw XCTSkip("Macros are only supported when running tests for the host platform") - // #endif - // } } From ec5d5dd906cd5b36b00f146b5d002f614aed6a60 Mon Sep 17 00:00:00 2001 From: hank121314 Date: Tue, 15 Sep 2026 01:08:33 +0800 Subject: [PATCH 3/4] docs: update docc --- .../Documentation.docc/Documentation.md | 1 + .../Documentation.docc/SerdeDefault.md | 84 +++++++++++++++++++ .../Documentation.docc/SupportedTypes.md | 5 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 Sources/Defaults/Documentation.docc/SerdeDefault.md diff --git a/Sources/Defaults/Documentation.docc/Documentation.md b/Sources/Defaults/Documentation.docc/Documentation.md index 2951c04d..149d038a 100644 --- a/Sources/Defaults/Documentation.docc/Documentation.md +++ b/Sources/Defaults/Documentation.docc/Documentation.md @@ -60,6 +60,7 @@ struct ContentView: View { ### Customize Storage +- - - ``Defaults/Serializable`` - ``Defaults/Bridge`` diff --git a/Sources/Defaults/Documentation.docc/SerdeDefault.md b/Sources/Defaults/Documentation.docc/SerdeDefault.md new file mode 100644 index 00000000..ea88f760 --- /dev/null +++ b/Sources/Defaults/Documentation.docc/SerdeDefault.md @@ -0,0 +1,84 @@ +# Using @SerdeDefault + +Use `@SerdeDefault` to make a struct compatible with Defaults without writing a custom bridge. + +## Overview + +The `@SerdeDefault` macro generates a ``Defaults/Serializable`` conformance from a struct's stored properties. Add the `DefaultsMacros` product to your target, import both modules, and attach the macro to your type: + +```swift +import Defaults +import DefaultsMacros + +@SerdeDefault +struct User { + var name: String + var age: UInt? +} + +extension Defaults.Keys { + static let user = Key( + "user", + default: User(name: "Appleseed", age: nil) + ) +} + +Defaults[.user].name +//=> "Appleseed" +``` + +The generated bridge stores the properties in a dictionary keyed by property name. Native Defaults values are stored directly. Other property types use their own ``Defaults/Serializable`` conformance, so you can compose models from other compatible types. + +```swift +@SerdeDefault +struct Profile { + var user: User + var tags: [String] +} +``` + +### Provide Fallback Values + +A property initializer is used as a fallback when the stored value is missing or cannot be deserialized. An optional property without an initializer falls back to `nil`. + +```swift +@SerdeDefault +struct Settings { + var displayName = "Anonymous" + var launchCount = 0 + var lastOpenedAt: Date? +} +``` + +A required property without an initializer must be present and valid. If it is not, the generated bridge cannot deserialize the struct and Defaults returns the key's default value. + +### Exclude a Property + +Use `@serde(.skip)` for optional state that should not be persisted. The generated bridge omits the property when serializing and initializes it to `nil` when deserializing. + +```swift +@SerdeDefault +struct User { + var name: String + + @serde(.skip) + var cachedAvatar: Data? +} +``` + +Only optional properties can use `@serde(.skip)`. + +### Understand the Limitations + +`@SerdeDefault` supports non-generic structs. Static and computed properties are ignored. Stored properties must either use a type supported by Defaults or conform to ``Defaults/Serializable``. + +> Warning: Renaming a stored property changes its dictionary key. Migrate previously stored values before renaming properties in a released app. + +> Warning: Using macros can increase build times because the `DefaultsMacros` product depends on `swift-syntax`. + +## Topics + +### Related APIs + +- ``Defaults/Serializable`` +- ``Defaults/Bridge`` diff --git a/Sources/Defaults/Documentation.docc/SupportedTypes.md b/Sources/Defaults/Documentation.docc/SupportedTypes.md index e365c60d..d3d12491 100644 --- a/Sources/Defaults/Documentation.docc/SupportedTypes.md +++ b/Sources/Defaults/Documentation.docc/SupportedTypes.md @@ -104,12 +104,15 @@ Defaults[.user].name ## Custom Types -You can easily add support for any custom type. See for how to add support for your own types. +You can generate ``Defaults/Serializable`` conformance for a struct with the `@SerdeDefault` macro. See to get started. + +For complete control over the stored representation, see to learn how to create a custom bridge. ## Topics ### Making Types Compatible +- - ``Defaults/Serializable`` - ``Defaults/Bridge`` From e6ec6d05a49ca84c55b91ff0d305119d48e78a17 Mon Sep 17 00:00:00 2001 From: hank121314 Date: Tue, 15 Sep 2026 01:26:46 +0800 Subject: [PATCH 4/4] docs: add minor comments --- .../SerdeDefault/Semantics/Properties.swift | 3 +++ .../SerdeDefault/Syntax/FunctionDecl.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift index 7db1d2e2..c85bbb40 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Semantics/Properties.swift @@ -1,6 +1,9 @@ import SwiftSyntax extension SerdeDefault { + /** + Represents the `SerdeDefault` properties of a nominal type. + */ struct Properties { let properties: [any Property] diff --git a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift index 17f33992..71de91d5 100644 --- a/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift +++ b/Sources/DefaultsMacrosDeclarations/SerdeDefault/Syntax/FunctionDecl.swift @@ -4,6 +4,9 @@ extension SerdeDefault { struct FunctionDecl { let syntax: FunctionDeclSyntax + /** + Convenience builder for constructing a function declaration. + */ class Builder { /** The generated function name.