├── .gitignore
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── ModifiedCopy
│ └── ModifiedCopy.swift
├── ModifiedCopyClient
│ └── main.swift
└── ModifiedCopyMacros
│ └── ModifiedCopyMacro.swift
└── Tests
└── ModifiedCopyTests
└── ModifiedCopyTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
92 | Package.resolved
93 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Wilhelm Oks
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import CompilerPluginSupport
6 |
7 | let package = Package(
8 | name: "ModifiedCopy",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10 | products: [
11 | // Products define the executables and libraries a package produces, making them visible to other packages.
12 | .library(
13 | name: "ModifiedCopy",
14 | targets: ["ModifiedCopy"]
15 | ),
16 | .executable(
17 | name: "ModifiedCopyClient",
18 | targets: ["ModifiedCopyClient"]
19 | ),
20 | ],
21 | dependencies: [
22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "509.1.1"),
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package, defining a module or a test suite.
27 | // Targets can depend on other targets in this package and products from dependencies.
28 | // Macro implementation that performs the source transformation of a macro.
29 | .macro(
30 | name: "ModifiedCopyMacros",
31 | dependencies: [
32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
34 | ]
35 | ),
36 |
37 | // Library that exposes a macro as part of its API, which is used in client programs.
38 | .target(name: "ModifiedCopy", dependencies: ["ModifiedCopyMacros"]),
39 |
40 | // A client of the library, which is able to use the macro in its own code.
41 | .executableTarget(name: "ModifiedCopyClient", dependencies: ["ModifiedCopy"]),
42 |
43 | // A test target used to develop the macro implementation.
44 | .testTarget(
45 | name: "ModifiedCopyTests",
46 | dependencies: [
47 | "ModifiedCopyMacros",
48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
49 | ]
50 | ),
51 | ]
52 | )
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | # ModifiedCopyMacro
10 | A Swift macro for making inline copies of a struct by modifying a property.
11 | The syntax is similar to Kotlin's copy function for data classes: https://kotlinlang.org/docs/data-classes.html#copying
12 |
13 | ## Usage
14 |
15 | Apply the `@Copyable` macro to a struct:
16 |
17 | ```swift
18 | @Copyable
19 | struct Person {
20 | let name: String
21 | var age: Int
22 | }
23 | ```
24 |
25 | and it will add a copy function for each stored property and constant:
26 | ```swift
27 | struct Person {
28 | let name: String
29 | var age: Int
30 |
31 | /// Returns a copy of the caller whose value for `name` is different.
32 | func copy(name: String) -> Self {
33 | .init(name: name, age: age)
34 | }
35 |
36 | /// Returns a copy of the caller whose value for `age` is different.
37 | func copy(age: Int) -> Self {
38 | .init(name: name, age: age)
39 | }
40 | }
41 | ```
42 |
43 | ## Capabilities, Limitations and Design Choices
44 |
45 | ### Chains for multiple changes
46 |
47 | To make a copy of a struct and modify multiple properties, you can chain the `copy` calls like this:
48 | `Person(name: "Walter White", age: 50).copy(age: 52).copy(name: "Heisenberg")`
49 |
50 | This is different than Kotlin's version of `copy`, which allows multiple parameters to pass in a single call.
51 | It's not possible to implement it like that in Swift because it's not possible to have default values for parameters which refer to the current values of the properties of the struct (or class).
52 | And we also can't use nil as a marker for the old/current value, because nil might be a valid new value that we want the property to set to when we make a copy.
53 | There might be a way that I'm not aware of, to still make it possible. So if you know how to do it, please let me know.
54 |
55 | #### CopyableCombi
56 |
57 | With version 2.1.0, the separate macro CopyableCombi was introduced, which generates copy functions with all combinations of parameters.
58 | This solution has the disadvantage that the number of generated functions can become large quickly, but it provides an API which is more similar to Kotlin's copy function.
59 |
60 | ### Stored properties and constants
61 |
62 | A copy function will be generated for each stored property (`var`) and each constant (`let`) of the struct.
63 | The macro recognizes computed properties by checking if they have `get` or `set` accessors.
64 |
65 | ### Only for struct
66 |
67 | This macro works only for structs.
68 | It doesn't make sense for enums because enums can't have stored properties.
69 | Classes and actors have reference semantics and I don't want this library to provide a copy function for reference types. I just want to augment the natural copy capability of structs with modified properties.
70 | This macro emits a Diagnostic Message when you try to apply it to anything but a struct.
71 |
72 | ### Only with default init
73 |
74 | The generated copy functions produce the copies by calling the synthesized default initializer.
75 | So, if you provide a custom initializer, no default initializer will be synthesized and the copy functions won't work.
76 | You can still have synthesized default initializers if you define your custom initializers in an extension.
77 |
78 | ### No struct extensions
79 |
80 | You can't apply this macro on an extension of a struct.
81 | This seems to be a limitation of the macro system.
82 | If you know how to make it possible, please let me know :)
83 |
84 | ## Installation
85 |
86 | Macros are a new language feature of Swift 5.9 and it will only work in Xcode 15 and later.
87 |
88 | Add the url `https://github.com/WilhelmOks/ModifiedCopyMacro.git` as a Swift Package to your project.
89 |
90 | When prompted, select the Package Product `ModifiedCopy` (Kind: Library) to add to your target.
91 |
92 | Make a clean build.
93 |
94 | `import ModifiedCopy` in the file where you want to attach the `@Copyable` macro.
95 |
--------------------------------------------------------------------------------
/Sources/ModifiedCopy/ModifiedCopy.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
4 | /// A macro that adds a `copy` function to a struct for each stored property that the struct contains.
5 | /// Each `copy` function returns a copy of the struct that the macro is attached on, but one property can be set to a differnet value.
6 | @attached(member, names: named(copy))
7 | public macro Copyable() = #externalMacro(module: "ModifiedCopyMacros", type: "ModifiedCopyMacro")
8 |
9 | @attached(member, names: named(copy))
10 | public macro CopyableCombi() = #externalMacro(module: "ModifiedCopyMacros", type: "ModifiedCopyCombiMacro")
11 |
--------------------------------------------------------------------------------
/Sources/ModifiedCopyClient/main.swift:
--------------------------------------------------------------------------------
1 | import ModifiedCopy
2 |
3 | @Copyable
4 | public struct Person {
5 | private(set) var name: String
6 |
7 | let age: Int
8 |
9 | private var favoriteColor: String
10 |
11 | /// This should not generate a copy function because it's not a stored property.
12 | var fullName: String {
13 | get {
14 | name
15 | }
16 | set {
17 | name = newValue
18 | }
19 | }
20 |
21 | /// This should not generate a copy function because it's not a stored property.
22 | var uppercasedName: String {
23 | name.uppercased()
24 | }
25 |
26 | var nickName: String? = "Bobby Tables" {
27 | didSet {
28 | print("nickName changed to \(nickName ?? "(nil)")")
29 | }
30 | }
31 |
32 | init(name: String, age: Int, favoriteColor: String, nickName: String? = nil) {
33 | self.name = name
34 | self.age = age
35 | self.favoriteColor = favoriteColor
36 | self.nickName = nickName
37 | }
38 | }
39 |
40 | print("Person copy with new age: \(Person(name: "Hank", age: 50, favoriteColor: "pink", nickName: "Hanky").copy(age: 42).age)")
41 |
42 | @CopyableCombi
43 | struct Song {
44 | let title: String
45 | let year: Int
46 | //let isFavorite: Bool
47 | //let rating: Double
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/ModifiedCopyMacros/ModifiedCopyMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 | import Foundation
3 | import SwiftSyntax
4 | import SwiftSyntaxBuilder
5 | import SwiftSyntaxMacros
6 | import SwiftDiagnostics
7 |
8 | enum ModifiedCopyDiagnostic: DiagnosticMessage {
9 | case notAStruct
10 | case propertyTypeProblem(PatternBindingListSyntax.Element)
11 |
12 | var severity: DiagnosticSeverity {
13 | switch self {
14 | case .notAStruct: .error
15 | case .propertyTypeProblem: .warning
16 | }
17 | }
18 |
19 | var message: String {
20 | switch self {
21 | case .notAStruct:
22 | "'@Copyable' can only be applied to a 'struct'"
23 | case .propertyTypeProblem(let binding):
24 | "Type error for property '\(binding.pattern)': \(binding)"
25 | }
26 | }
27 |
28 | var diagnosticID: MessageID {
29 | switch self {
30 | case .notAStruct:
31 | .init(domain: "ModifiedCopyMacros", id: "notAStruct")
32 | case .propertyTypeProblem(let binding):
33 | .init(domain: "ModifiedCopyMacros", id: "propertyTypeProblem(\(binding.pattern))")
34 | }
35 | }
36 | }
37 |
38 | public struct ModifiedCopyMacro: MemberMacro {
39 | public static func expansion(
40 | of node: AttributeSyntax,
41 | providingMembersOf declaration: some DeclGroupSyntax,
42 | in context: some MacroExpansionContext
43 | ) throws -> [DeclSyntax] {
44 | guard let structDeclSyntax = declaration as? StructDeclSyntax else {
45 | let diagnostic = Diagnostic(node: Syntax(node), message: ModifiedCopyDiagnostic.notAStruct)
46 | context.diagnose(diagnostic)
47 | return []
48 | }
49 |
50 | let structVisibility = structDeclSyntax.modifiers.visibilityText() ?? "internal"
51 |
52 | let variables = structDeclSyntax.memberBlock.members.compactMap { $0.decl.as(VariableDeclSyntax.self) }
53 |
54 | let bindings = variables.flatMap(\.bindings).filter { accessorIsAllowed($0.accessorBlock?.accessors) }
55 |
56 | return variables.flatMap { variable in
57 | let variableVisibility = variable.modifiers.visibilityText() ?? structVisibility
58 |
59 | return variable.bindings
60 | .filter { accessorIsAllowed($0.accessorBlock?.accessors) }
61 | .compactMap { binding -> DeclSyntax? in
62 | let propertyName = binding.pattern
63 | guard let typeName = binding.typeAnnotation?.type else {
64 | let diagnostic = Diagnostic(node: Syntax(node), message: ModifiedCopyDiagnostic.propertyTypeProblem(binding))
65 | context.diagnose(diagnostic)
66 | return nil
67 | }
68 |
69 |
70 | return """
71 | /// Returns a copy of the caller whose value for `\(propertyName)` is different.
72 | \(raw: variableVisibility) func copy(\(propertyName): \(typeName.trimmed)) -> Self {
73 | .init(\(raw: bindings.map { "\($0.pattern): \($0.pattern)" }.joined(separator: ", ")))
74 | }
75 | """
76 | }
77 | }
78 | }
79 | }
80 |
81 | public struct ModifiedCopyCombiMacro: MemberMacro {
82 | public static func expansion(
83 | of node: AttributeSyntax,
84 | providingMembersOf declaration: some DeclGroupSyntax,
85 | in context: some MacroExpansionContext
86 | ) throws -> [DeclSyntax] {
87 | guard let structDeclSyntax = declaration as? StructDeclSyntax else {
88 | let diagnostic = Diagnostic(node: Syntax(node), message: ModifiedCopyDiagnostic.notAStruct)
89 | context.diagnose(diagnostic)
90 | return []
91 | }
92 |
93 | let structVisibility = structDeclSyntax.modifiers.visibilityText() ?? "internal"
94 |
95 | let variables = structDeclSyntax.memberBlock.members
96 | .compactMap { $0.decl.as(VariableDeclSyntax.self) }
97 | .filter { $0.bindings.allSatisfy { accessorIsAllowed($0.accessorBlock?.accessors) } }
98 |
99 | let bindings = variables.flatMap(\.bindings).filter { accessorIsAllowed($0.accessorBlock?.accessors) }
100 |
101 | let variablesCombi = variables.combinationsWithoutRepetition.filter { !$0.isEmpty }
102 |
103 | return variablesCombi.compactMap { variableCombi -> DeclSyntax? in
104 | let bindingsCombi = variableCombi.flatMap { $0.bindings }
105 | let many = bindingsCombi.count > 1
106 |
107 | let propertyNamesString = bindingsCombi.map { "`\($0.pattern)`" }.joined(separator: " and ")
108 | let parameterListString = bindingsCombi.map { binding in "\(binding.pattern): \(binding.typeAnnotation?.type.trimmed ?? "?")" }.joined(separator: ", ")
109 |
110 | return """
111 | /// Returns a copy of the caller whose \(raw: many ? "values" : "value") for \(raw: propertyNamesString) \(raw: many ? "are" : "is") different.
112 | \(raw: structVisibility) func copy(\(raw: parameterListString)) -> Self {
113 | .init(\(raw: bindings.map { "\($0.pattern): \($0.pattern)" }.joined(separator: ", ")))
114 | }
115 | """
116 | }
117 | }
118 | }
119 |
120 | private func accessorIsAllowed(_ accessor: AccessorBlockSyntax.Accessors?) -> Bool {
121 | guard let accessor else { return true }
122 | return switch accessor {
123 | case .accessors(let accessorDeclListSyntax):
124 | !accessorDeclListSyntax.contains {
125 | $0.accessorSpecifier.text == "get" || $0.accessorSpecifier.text == "set"
126 | }
127 | case .getter:
128 | false
129 | }
130 | }
131 |
132 | extension DeclModifierListSyntax {
133 | private static let visibilityModifiers: Set = ["private", "fileprivate", "internal", "package", "public", "open"]
134 |
135 | func visibilityText() -> String? {
136 | self.map(\.name.text)
137 | .first(where: { Self.visibilityModifiers.contains($0) })
138 | }
139 | }
140 |
141 | extension Array {
142 | var combinationsWithoutRepetition: [[Element]] {
143 | guard !isEmpty else { return [[]] }
144 | return Array(self[1...]).combinationsWithoutRepetition.flatMap { [$0, [self[0]] + $0] }
145 | }
146 | }
147 |
148 | @main
149 | struct ModifiedCopyPlugin: CompilerPlugin {
150 | let providingMacros: [Macro.Type] = [
151 | ModifiedCopyMacro.self,
152 | ModifiedCopyCombiMacro.self,
153 | ]
154 | }
155 |
--------------------------------------------------------------------------------
/Tests/ModifiedCopyTests/ModifiedCopyTests.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntaxMacros
2 | import SwiftSyntaxMacrosTestSupport
3 | import XCTest
4 | import ModifiedCopyMacros
5 | import ModifiedCopy
6 |
7 | let testMacros: [String: Macro.Type] = [
8 | "Copyable": ModifiedCopyMacro.self,
9 | "CopyableCombi": ModifiedCopyCombiMacro.self,
10 | ]
11 |
12 | @Copyable
13 | struct Person: Equatable {
14 | var name: String
15 |
16 | let age: Int
17 |
18 | /// This should not generate a copy function because it's not a stored property.
19 | var fullName: String {
20 | get {
21 | name
22 | }
23 | set {
24 | name = newValue
25 | }
26 | }
27 |
28 | /// This should not generate a copy function because it's not a stored property.
29 | var uppercasedName: String {
30 | name.uppercased()
31 | }
32 |
33 | var nickName: String? = "Bobby Tables" {
34 | didSet {
35 | print("nickName changed to \(nickName ?? "(nil)")")
36 | }
37 | }
38 | }
39 |
40 | final class ModifiedCopyTests: XCTestCase {
41 | func testMacroExpansion() {
42 | assertMacroExpansion(
43 | #"""
44 | @Copyable
45 | public struct Person {
46 | private(set) var name: String
47 |
48 | let age: Int
49 |
50 | private var favoriteColor: String
51 |
52 | /// This should not generate a copy function because it's not a stored property.
53 | var fullName: String {
54 | get {
55 | name
56 | }
57 | set {
58 | name = newValue
59 | }
60 | }
61 |
62 | /// This should not generate a copy function because it's not a stored property.
63 | var uppercasedName: String {
64 | name.uppercased()
65 | }
66 |
67 | var nickName: String? = "Bobby Tables" {
68 | didSet {
69 | print("nickName changed to \(nickName ?? "(nil)")")
70 | }
71 | }
72 |
73 | init(name: String, age: Int, favoriteColor: String, nickName: String? = nil) {
74 | self.name = name
75 | self.age = age
76 | self.favoriteColor = favoriteColor
77 | self.nickName = nickName
78 | }
79 | }
80 | """#,
81 | expandedSource:
82 | #"""
83 | public struct Person {
84 | private(set) var name: String
85 |
86 | let age: Int
87 |
88 | private var favoriteColor: String
89 |
90 | /// This should not generate a copy function because it's not a stored property.
91 | var fullName: String {
92 | get {
93 | name
94 | }
95 | set {
96 | name = newValue
97 | }
98 | }
99 |
100 | /// This should not generate a copy function because it's not a stored property.
101 | var uppercasedName: String {
102 | name.uppercased()
103 | }
104 |
105 | var nickName: String? = "Bobby Tables" {
106 | didSet {
107 | print("nickName changed to \(nickName ?? "(nil)")")
108 | }
109 | }
110 |
111 | init(name: String, age: Int, favoriteColor: String, nickName: String? = nil) {
112 | self.name = name
113 | self.age = age
114 | self.favoriteColor = favoriteColor
115 | self.nickName = nickName
116 | }
117 |
118 | /// Returns a copy of the caller whose value for `name` is different.
119 | private func copy(name: String) -> Self {
120 | .init(name: name, age: age, favoriteColor: favoriteColor, nickName: nickName)
121 | }
122 |
123 | /// Returns a copy of the caller whose value for `age` is different.
124 | public func copy(age: Int) -> Self {
125 | .init(name: name, age: age, favoriteColor: favoriteColor, nickName: nickName)
126 | }
127 |
128 | /// Returns a copy of the caller whose value for `favoriteColor` is different.
129 | private func copy(favoriteColor: String) -> Self {
130 | .init(name: name, age: age, favoriteColor: favoriteColor, nickName: nickName)
131 | }
132 |
133 | /// Returns a copy of the caller whose value for `nickName` is different.
134 | public func copy(nickName: String?) -> Self {
135 | .init(name: name, age: age, favoriteColor: favoriteColor, nickName: nickName)
136 | }
137 | }
138 | """#,
139 | macros: testMacros
140 | )
141 | }
142 |
143 | func testCombiMacroExpansion() {
144 | assertMacroExpansion(
145 | #"""
146 | @CopyableCombi
147 | public struct Person {
148 | let name: String
149 | let age: Int
150 | }
151 | """#,
152 | expandedSource:
153 | #"""
154 | public struct Person {
155 | let name: String
156 | let age: Int
157 |
158 | /// Returns a copy of the caller whose value for `name` is different.
159 | public func copy(name: String) -> Self {
160 | .init(name: name, age: age)
161 | }
162 |
163 | /// Returns a copy of the caller whose value for `age` is different.
164 | public func copy(age: Int) -> Self {
165 | .init(name: name, age: age)
166 | }
167 |
168 | /// Returns a copy of the caller whose values for `name` and `age` are different.
169 | public func copy(name: String, age: Int) -> Self {
170 | .init(name: name, age: age)
171 | }
172 | }
173 | """#,
174 | macros: testMacros
175 | )
176 | }
177 |
178 | func testNewLetValue() {
179 | let person = Person(name: "Walter White", age: 50, nickName: "Heisenberg")
180 | let copiedPerson = person.copy(age: 51)
181 | XCTAssertEqual(Person(name: "Walter White", age: 51, nickName: "Heisenberg"), copiedPerson)
182 | }
183 |
184 | func testNewVarValue() {
185 | let person = Person(name: "Walter White", age: 50, nickName: "Heisenberg")
186 | let copiedPerson = person.copy(name: "W.W.")
187 | XCTAssertEqual(Person(name: "W.W.", age: 50, nickName: "Heisenberg"), copiedPerson)
188 | }
189 |
190 | func testNewOptionalValue() {
191 | let person = Person(name: "Walter White", age: 50, nickName: "Heisenberg")
192 | let copiedPerson = person.copy(nickName: nil)
193 | XCTAssertEqual(Person(name: "Walter White", age: 50, nickName: nil), copiedPerson)
194 | }
195 |
196 | func testChainedNewValues() {
197 | let person = Person(name: "Walter White", age: 50, nickName: "Heisenberg")
198 | let copiedPerson = person.copy(name: "Skyler White").copy(age: 48)
199 | XCTAssertEqual(Person(name: "Skyler White", age: 48, nickName: "Heisenberg"), copiedPerson)
200 | }
201 | }
202 |
--------------------------------------------------------------------------------