├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── WeakMacro │ └── WeakMacro.swift ├── WeakMacroExample │ └── main.swift └── WeakMacroPlugin │ ├── Plugin.swift │ └── WeakifyMacro.swift └── Tests └── WeakTests └── Tests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-syntax.git", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "ee682e50abd21fec41207a75832992a1a485d5df" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /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: "WeakMacro", 9 | platforms: [ .iOS("13.0"), .macOS("10.15")], 10 | products: [ 11 | .library(name: "WeakMacro", targets: ["WeakMacro"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-syntax.git",branch: "main"), 15 | ], 16 | targets: [ 17 | .macro( 18 | name: "WeakMacroPlugin", 19 | dependencies: [ 20 | .product(name: "SwiftSyntax", package: "swift-syntax"), 21 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 22 | .product(name: "SwiftOperators", package: "swift-syntax"), 23 | .product(name: "SwiftParser", package: "swift-syntax"), 24 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), 25 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 26 | ] 27 | ), 28 | .executableTarget(name: "WeakMacroExample", dependencies: ["WeakMacro"]), 29 | .target(name: "WeakMacro", dependencies: ["WeakMacroPlugin"]), 30 | .testTarget(name: "WeakTests", dependencies: ["WeakMacroPlugin"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Macro for Weakify 2 | 3 | Usage: 4 | 5 | Use `#weak` with a member access expression on `self`: 6 | ```swift 7 | let a = #weak(self.onClick(button:_:)) 8 | ``` 9 | 10 | expands into: 11 | 12 | ```swift 13 | let a = {[weak self] _arg0, _arg1 in if let self {self.onClick(button: _arg0, _arg1)}} 14 | ``` 15 | 16 | There is an example module `WeakMacroExample` 17 | 18 | *Also, yes. I know, I should have used `context.makeUniqueName()` :) * 19 | -------------------------------------------------------------------------------- /Sources/WeakMacro/WeakMacro.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | //@freestanding(expression) public macro weak(_ function: (repeat each A) -> R) -> (repeat each A) -> R = #externalMacro(module: "WeakMacroPlugin", type: "WeakifyMacro") 4 | 5 | @freestanding(expression) public macro weak(_ function: (repeat each A) -> R) -> (repeat each A) -> R = #externalMacro(module: "WeakMacroPlugin", type: "WeakifyMacro") 6 | -------------------------------------------------------------------------------- /Sources/WeakMacroExample/main.swift: -------------------------------------------------------------------------------- 1 | import WeakMacro 2 | 3 | class A { 4 | private(set) var handler: ((Int, Bool) -> Void)? 5 | 6 | init() {} 7 | 8 | func handler(_ arg0: Int, arg1: Bool) { 9 | print("handled \(arg0) and \(arg1)") 10 | } 11 | 12 | func accept(handler: @escaping (Int, Bool) -> Void) { 13 | self.handler = handler 14 | } 15 | 16 | func foo() { 17 | accept(handler: #weak(self.handler(_:arg1:))) 18 | } 19 | 20 | deinit { print("deinit") } 21 | } 22 | 23 | var a: A? = A() 24 | a?.foo() 25 | a?.handler?(1, true) 26 | a = nil 27 | print("end") 28 | -------------------------------------------------------------------------------- /Sources/WeakMacroPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftCompilerPlugin) 2 | import SwiftCompilerPlugin 3 | import SwiftSyntaxMacros 4 | 5 | @main 6 | struct WeakMacroPlugin: CompilerPlugin { 7 | let providingMacros: [Macro.Type] = [ 8 | WeakifyMacro.self 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/WeakMacroPlugin/WeakifyMacro.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | 7 | enum WeakifyMacroDiagnostic: String, DiagnosticMessage { 8 | case notAnAccessExpr 9 | case memberAccessNotSelf 10 | 11 | var severity: SwiftDiagnostics.DiagnosticSeverity { .error } 12 | var message: String { 13 | switch self { 14 | case .memberAccessNotSelf: 15 | return "#weak can only be applied to expressions on `self`" 16 | case .notAnAccessExpr: 17 | return "#weak requires member access expression as an argument" 18 | } 19 | } 20 | var diagnosticID: SwiftDiagnostics.MessageID { MessageID(domain: "WeakifyMacro", id: rawValue) } 21 | } 22 | 23 | public struct WeakifyMacro: ExpressionMacro { 24 | public static func expansion( 25 | of node: some FreestandingMacroExpansionSyntax, 26 | in context: some MacroExpansionContext 27 | ) -> ExprSyntax { 28 | guard let argument = node.argumentList.first?.expression.as(MemberAccessExprSyntax.self) else { 29 | context.diagnose(.init(node: node._syntaxNode, message: WeakifyMacroDiagnostic.notAnAccessExpr)) 30 | return "" 31 | } 32 | 33 | guard argument.base?.as(IdentifierExprSyntax.self)?.identifier.tokenKind == .keyword(.`self`) else { 34 | context.diagnose(.init(node: node._syntaxNode, message: WeakifyMacroDiagnostic.memberAccessNotSelf)) 35 | return "" 36 | } 37 | let name = argument.name.text 38 | let args = argument.declNameArguments?.arguments.map(\.name.text) ?? [] 39 | let argumentList = args.enumerated().map { "_arg\($0.offset)" }.joined(separator: ", ") + (args.isEmpty ? "" : " ") 40 | let invocationArgumentList = args.enumerated().map { item in 41 | var accumulator = "" 42 | if item.element != "_" { 43 | accumulator += item.element + ": " 44 | } 45 | accumulator += "_arg\(item.offset)" 46 | return accumulator 47 | }.joined(separator: ", ") 48 | 49 | return "{[weak self] \(raw: argumentList)in if let self {self.\(raw: name)(\(raw: invocationArgumentList))}}" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/WeakTests/Tests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import WeakMacroPlugin 5 | import XCTest 6 | 7 | var testMacros: [String: Macro.Type] = [ 8 | "weak" : WeakifyMacro.self, 9 | ] 10 | 11 | final class MacroExamplesPluginTests: XCTestCase { 12 | func testWeakify() { 13 | let sf: SourceFileSyntax = 14 | #""" 15 | let a = #weak(self.onClick) 16 | """# 17 | let context = BasicMacroExpansionContext.init( 18 | sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] 19 | ) 20 | let transformedSF = sf.expand(macros: testMacros, in: context) 21 | XCTAssertEqual( 22 | transformedSF.description, 23 | #""" 24 | let a = {[weak self] in if let self {self.onClick()}} 25 | """# 26 | ) 27 | } 28 | 29 | func testWeakifyArgs() { 30 | let sf: SourceFileSyntax = 31 | #""" 32 | let a = #weak(self.onClick(button:_:)) 33 | """# 34 | let context = BasicMacroExpansionContext.init( 35 | sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] 36 | ) 37 | let transformedSF = sf.expand(macros: testMacros, in: context) 38 | XCTAssertEqual( 39 | transformedSF.description, 40 | #""" 41 | let a = {[weak self] _arg0, _arg1 in if let self {self.onClick(button: _arg0, _arg1)}} 42 | """# 43 | ) 44 | } 45 | } 46 | --------------------------------------------------------------------------------