Skip to content

Commit 0c44bca

Browse files
committed
add macro for adding mutating func for struct
1 parent 1536901 commit 0c44bca

File tree

9 files changed

+236
-0
lines changed

9 files changed

+236
-0
lines changed

Components/Macro/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

Components/Macro/Package.resolved

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Components/Macro/Package.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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: "Macro",
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: "Macro",
14+
targets: ["Macro"]
15+
),
16+
.executable(
17+
name: "MacroClient",
18+
targets: ["MacroClient"]
19+
),
20+
],
21+
dependencies: [
22+
// Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23+
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
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: "Macros",
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: "Macro",
39+
dependencies: [
40+
"Macros"
41+
]
42+
),
43+
44+
// A client of the library, which is able to use the macro in its own code.
45+
.executableTarget(name: "MacroClient", dependencies: ["Macro"]),
46+
47+
// A test target used to develop the macro implementation.
48+
.testTarget(
49+
name: "MacroTests",
50+
dependencies: [
51+
"Macro",
52+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
53+
]
54+
),
55+
]
56+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
/// A macro which generate `mutating func` for all variables inside struct.
5+
@attached(member, names: named(set))
6+
public macro Mutable() = #externalMacro(module: "Macros", type: "MutableMacro")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Macro
2+
3+
// MARK: - Precondition
4+
5+
@Mutable
6+
struct SomeStruct {
7+
let id: Int = Int.random(in: Int.min...Int.max)
8+
var someFlag: Bool = false
9+
var someString: String = ""
10+
}
11+
12+
// MARK: - Helpers
13+
14+
extension SomeStruct {
15+
16+
func debugPrintAll() {
17+
debugPrint(someFlag)
18+
debugPrint(someString)
19+
}
20+
21+
}
22+
23+
// MARK: - Debugging
24+
25+
var someStruct = SomeStruct()
26+
27+
someStruct.debugPrintAll()
28+
29+
someStruct.set(someFlag: true)
30+
someStruct.set(someString: "echo")
31+
32+
someStruct.debugPrintAll()
33+
34+
someStruct.set(someFlag: false)
35+
someStruct.set(someString: "horray")
36+
37+
someStruct.debugPrintAll()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// MacroError.swift
3+
//
4+
//
5+
// Created by Никита Коробейников on 16.06.2023.
6+
//
7+
8+
public enum MacroError: Error {
9+
10+
case onlyApplicableToStruct
11+
case typeAnnotationRequiredFor(variableName: String)
12+
13+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
6+
/// A macro which generate `mutating func` for all variables inside struct.
7+
public struct MutableMacro: MemberMacro {
8+
9+
public static func expansion<Declaration, Context>(of node: AttributeSyntax,
10+
providingMembersOf declaration: Declaration,
11+
in context: Context) throws -> [DeclSyntax]
12+
where Declaration: DeclGroupSyntax,
13+
Context: MacroExpansionContext {
14+
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
15+
throw MacroError.onlyApplicableToStruct
16+
}
17+
18+
let variables = structDecl.memberBlock
19+
.members
20+
21+
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
22+
.filter { $0.bindingKeyword.text == "var" }
23+
24+
let functions = try variables.compactMap { variableDecl -> FunctionDeclSyntax? in
25+
guard let variableBinding = variableDecl.bindings.first,
26+
let variableType = variableBinding.typeAnnotation?.type else {
27+
throw MacroError.typeAnnotationRequiredFor(variableName: variableDecl.bindings.first?.pattern.description ?? "unknown")
28+
}
29+
30+
let variableName = TokenSyntax(stringLiteral: variableBinding.pattern.description)
31+
32+
let parameter = FunctionParameterSyntax(firstName: variableName, type: variableType)
33+
let parameterList = FunctionParameterListSyntax(arrayLiteral: parameter)
34+
let modifiers = ModifierListSyntax(arrayLiteral: .init(name: .keyword(.mutating)))
35+
let bodyItem = CodeBlockItemSyntax.Item.expr(.init(stringLiteral: "self.\(variableName.text)=\(variableName.text)"))
36+
let body = CodeBlockSyntax(statements: .init(arrayLiteral: .init(item: bodyItem)))
37+
return FunctionDeclSyntax(modifiers: modifiers,
38+
identifier: .identifier("set"),
39+
signature: .init(input: .init(parameterList: parameterList)),
40+
body: body
41+
)
42+
}
43+
44+
return functions.compactMap { $0.as(DeclSyntax.self) }
45+
}
46+
47+
}
48+
49+
@main
50+
struct MacroPlugin: CompilerPlugin {
51+
let providingMacros: [Macro.Type] = [
52+
MutableMacro.self,
53+
]
54+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftSyntaxMacros
2+
import SwiftSyntaxMacrosTestSupport
3+
import XCTest
4+
import Macros
5+
6+
let testMacros: [String: Macro.Type] = [
7+
"mutable": MutableMacro.self,
8+
]
9+
10+
final class MutableMacroTests: XCTestCase {
11+
12+
func testMutableMacro_expansion() {
13+
assertMacroExpansion(
14+
"""
15+
@Mutable
16+
struct SomeStruct {
17+
let id: String = ""
18+
var someVar: Bool = false
19+
}
20+
""",
21+
expandedSource: """
22+
@Mutable
23+
struct SomeStruct {
24+
let id: String = ""
25+
var someVar: Bool = false
26+
27+
mutating func set(someVar: Bool) {
28+
self.someVar = someVar
29+
}
30+
}
31+
""",
32+
macros: testMacros
33+
)
34+
}
35+
36+
func testMutableMacro_application() {
37+
// TODO: - check that macro throwing error
38+
}
39+
40+
}

0 commit comments

Comments
 (0)