├── .gitignore ├── .spi.yml ├── Sources ├── Renamed │ └── Macros.swift └── RenamedPlugin │ ├── RenamedPlugin.swift │ ├── SyntaxStringInterpolation+Optional.swift │ └── Renamed.swift ├── .vscode └── settings.json ├── Package.resolved ├── .github └── workflows │ └── test.yml ├── LICENSE ├── Package.swift ├── README.md └── Tests └── RenamedTests └── RenamedTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | .DS_store 4 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: ["Renamed", "RenamedPlugin"] 5 | swift_version: 5.9 6 | -------------------------------------------------------------------------------- /Sources/Renamed/Macros.swift: -------------------------------------------------------------------------------- 1 | @attached(peer, names: arbitrary) 2 | public macro Renamed(from previousName: String) = #externalMacro(module: "RenamedPlugin", type: "Renamed") 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "swift.path": "/Library/Developer/Toolchains/swift-5.9-DEVELOPMENT-SNAPSHOT-2023-06-05-a.xctoolchain/usr/bin", 3 | "swift.swiftEnvironmentVariables": { 4 | "TOOLCHAINS": "org.swift.59202306051a" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/RenamedPlugin/RenamedPlugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct RenamedPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | Renamed.self, 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Sources/RenamedPlugin/SyntaxStringInterpolation+Optional.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | extension SyntaxStringInterpolation { 5 | internal mutating func appendInterpolation( 6 | optional node: Node? 7 | ) { 8 | guard let node else { return } 9 | self.appendInterpolation(node) 10 | } 11 | } -------------------------------------------------------------------------------- /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" : "c28f9940ba2b89b25f1e754762df0253ca20ccdf" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: macos-latest 9 | env: 10 | TOOLCHAINS: org.swift.59202305161a 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Install Swift Development Snapshot 14 | uses: slashmo/install-swift@v0.4.0 15 | with: 16 | version: swift-DEVELOPMENT-SNAPSHOT-2023-05-14-a 17 | - name: Build 18 | run: swift build 19 | - name: Test 20 | run: swift test -v 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Joseph Duffy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import CompilerPluginSupport 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Renamed", 7 | platforms: [ 8 | .macOS(.v10_15), 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6), 12 | .macCatalyst(.v13), 13 | ], 14 | products: [ 15 | .library( 16 | name: "Renamed", 17 | targets: ["Renamed"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/apple/swift-syntax.git", 23 | branch: "main" 24 | ), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "Renamed", 29 | dependencies: [ 30 | "RenamedPlugin", 31 | ] 32 | ), 33 | .macro( 34 | name: "RenamedPlugin", 35 | dependencies: [ 36 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 37 | .product(name: "SwiftSyntax", package: "swift-syntax"), 38 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 39 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 40 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 41 | ] 42 | ), 43 | .testTarget( 44 | name: "RenamedTests", 45 | dependencies: [ 46 | "RenamedPlugin", 47 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 49 | ] 50 | ), 51 | ] 52 | ) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Renamed 2 | 3 | Renamed is a macro to ease the renaming of symbols. For example, say you have 4 | 5 | ```swift 6 | struct MyType { 7 | let propertyName: String 8 | } 9 | ``` 10 | 11 | but you rename `MyType.propertyName` to `MyType.newName`. To avoid introducing a breaking change you can add a computed property named `propertyName` decorated with `@available(*, deprecated, renamed: "newName")`, but this can be cumbersome. 12 | 13 | With Renamed this can be done with a single attribute. 14 | 15 | ```swift 16 | struct MyType { 17 | @Renamed(from: "propertyName") 18 | let newProperty: String 19 | } 20 | ``` 21 | 22 | This generates the Swift code: 23 | 24 | ```swift 25 | struct MyType { 26 | let newName: String 27 | 28 | @available(*, deprecated, renamed: "newName") 29 | var propertyName: String { 30 | newName 31 | } 32 | } 33 | ``` 34 | 35 | (this isn't _exactly_ how the macro gets expanded, but that's not important here). 36 | 37 | `Renamed` uses the same naming syntax as the `renamed` parameter of the `@available` attribute. For example, renaming a function is also supported. 38 | 39 | ```swift 40 | @Renamed(from: "oldFunctionName(argument:_)") 41 | func newFunctionName(_ variableName: String, argumentName: Int) -> Bool {} 42 | ``` 43 | 44 | produces: 45 | 46 | ```swift 47 | func oldFunctionName(argument arg0: String, _ arg1: Int) -> Bool { 48 | newFunctionName(arg0, argumentName: arg1) 49 | } 50 | ``` 51 | 52 | Functions without parameters can omit the parentheses: 53 | 54 | ```swift 55 | @Renamed(from: "oldFunctionName") 56 | func newFunctionName() -> Bool {} 57 | ``` 58 | 59 | This works for: 60 | 61 | - [x] Structs 62 | - [x] Classes 63 | - [x] Enums 64 | - [x] Type aliases 65 | - [x] Immutable properties 66 | - [x] Mutable properties 67 | - [x] Functions 68 | 69 | ## FAQ 70 | 71 | **Q: How ready is this?** 72 | 73 | This is currently in beta, with support for the latest Swift 5.9 snapshot, specifically tested with the Xcode 15 beta. There are probably more cases that can be supported and most descriptive errors that could be thrown in some cases (e.g. rather than producing invalid code). 74 | 75 | With all that said, all supported use-cases have associated tests that prove this works. 76 | 77 | **Q: I found something that doesn't work. What can I do?** 78 | 79 | **A:** Please open an issue or, better yet, a PR with a failing or implementation. I'm sure there are some edge cases I've not thought of! 80 | 81 | **Q: Will this be slow?** 82 | 83 | **A:** There is a small compile-time hit but it's minimal. The code produced is often identical to what would be written by hand. No runtime dependencies are added. 84 | 85 | **Q: Do I have to add a whole dependency just to save myself some typing?** 86 | 87 | **A:** Yes. Personally I would only consider using this project for a big refactor. 88 | 89 | **Q: Is this type-safe?** 90 | 91 | **A:** To a degree. Swift will check the code generated by this marco, so if you enter an invalid name, e.g. `@Renamed(from: "1nvalid")` the compiler will throw an error. The macro will also produce a compile-time error when used on unsupported symbols, such as a variable without an explicit type. These errors are being worked on and some more situations will be supported. 92 | 93 | **Q: Where can I find more about macros?** 94 | 95 | **A:** [Swift Macros Dashboard](https://gist.github.com/DougGregor/de840fcf6d6f307792121eee11c0da85). [CustomHashable](https://github.com/JosephDuffy/CustomHashable). 96 | 97 | ## Installation 98 | 99 | To use macros you need to use a recent Swift toolchain. Because Xcode does not support building a package with a toolchain development is being done in Visual Studio Code. Check the [settings.json](./.vscode/settings.json) file to see which snapshot the project is being developed against. 100 | 101 | ## License 102 | 103 | [MIT](./LICENSE) 104 | -------------------------------------------------------------------------------- /Tests/RenamedTests/RenamedTests.swift: -------------------------------------------------------------------------------- 1 | import RenamedPlugin 2 | import SwiftSyntaxMacros 3 | import SwiftSyntaxMacrosTestSupport 4 | import XCTest 5 | 6 | private let testMacros: [String: Macro.Type] = [ 7 | "Renamed": Renamed.self, 8 | ] 9 | 10 | final class RenamedTests: XCTestCase { 11 | func testTypealiasForStruct() { 12 | assertMacroExpansion( 13 | """ 14 | @Renamed(from: "OldStruct") 15 | private struct RenamedStruct {} 16 | """, 17 | expandedSource: """ 18 | 19 | private struct RenamedStruct { 20 | } 21 | @available(*, deprecated, renamed: "RenamedStruct") 22 | private typealias OldStruct = RenamedStruct 23 | """, 24 | macros: testMacros 25 | ) 26 | } 27 | 28 | func testTypealiasForClass() { 29 | assertMacroExpansion( 30 | """ 31 | @Renamed(from: "OldClass") 32 | final class RenamedClass {} 33 | """, 34 | expandedSource: """ 35 | 36 | final class RenamedClass { 37 | } 38 | @available(*, deprecated, renamed: "RenamedClass") 39 | typealias OldClass = RenamedClass 40 | """, 41 | macros: testMacros 42 | ) 43 | } 44 | 45 | func testTypealiasForEnum() { 46 | assertMacroExpansion( 47 | """ 48 | @Renamed(from: "OldEnum") 49 | enum RenamedEnum {} 50 | """, 51 | expandedSource: """ 52 | 53 | enum RenamedEnum { 54 | } 55 | @available(*, deprecated, renamed: "RenamedEnum") 56 | typealias OldEnum = RenamedEnum 57 | """, 58 | macros: testMacros 59 | ) 60 | } 61 | 62 | func testTypealiasForTypealias() { 63 | assertMacroExpansion( 64 | """ 65 | @available(iOS 14, message: "Test Message!") 66 | @Renamed(from: "OldTypealias") 67 | public typealias RenamedTypealias = String 68 | 69 | @Renamed(from: "OldTypealias2") 70 | public typealias RenamedTypealias2 = String 71 | """, 72 | expandedSource: """ 73 | @available(iOS 14, message: "Test Message!") 74 | public typealias RenamedTypealias = String 75 | @available(iOS 14, deprecated, renamed: "RenamedTypealias") 76 | public typealias OldTypealias = RenamedTypealias 77 | public typealias RenamedTypealias2 = String 78 | @available(*, deprecated, renamed: "RenamedTypealias2") 79 | public typealias OldTypealias2 = RenamedTypealias2 80 | """, 81 | macros: testMacros 82 | ) 83 | } 84 | 85 | func testRenamedProperties() { 86 | // The output shows how some of the indentation, especially when scopes are provides, does 87 | // not match the surrounding code. This could be improved but isn't necessary for the 88 | // function of the macro. 89 | assertMacroExpansion( 90 | """ 91 | struct TestStruct { 92 | @Renamed(from: "oldImmutableProperty") 93 | let immutableProperty: String 94 | 95 | @Renamed(from: "oldMutableProperty") 96 | var mutableProperty: String 97 | 98 | @Renamed(from: "oldComputedImmutableProperty") 99 | var computedImmutableProperty: String { 100 | _computedImmutableProperty 101 | } 102 | 103 | private let _computedImmutableProperty: String 104 | 105 | @Renamed(from: "oldComputedMutableProperty") 106 | var computedMutableProperty: String { 107 | get { 108 | _computedMutableProperty 109 | } 110 | set { 111 | _computedMutableProperty = newValue 112 | } 113 | } 114 | 115 | private var _computedMutableProperty: String 116 | 117 | init(immutableProperty: String, mutableProperty: String, computedImmutableProperty: String, computedMutableProperty: String) { 118 | self.immutableProperty = immutableProperty 119 | self.mutableProperty = mutableProperty 120 | _computedImmutableProperty = computedImmutableProperty 121 | _computedMutableProperty = computedMutableProperty 122 | } 123 | 124 | @Renamed(from: "oldTestFunction(_:oldArgumentLabel:_:oldTrailingClosure)") 125 | public func testFunction(_ unnamedParameter: String, argumentLabel parameterName: Int, previouslyUnnamed: Bool, trailingClosure: @escaping () -> Void) -> Bool { 126 | true 127 | } 128 | 129 | @Renamed(from: "oldTestFunctionWithoutReturn") 130 | func testFunctionWithoutReturn() {} 131 | } 132 | """, 133 | expandedSource: """ 134 | struct TestStruct { 135 | let immutableProperty: String 136 | @available(*, deprecated, renamed: "immutableProperty") 137 | var oldImmutableProperty: String { 138 | immutableProperty 139 | } 140 | var mutableProperty: String 141 | @available(*, deprecated, renamed: "mutableProperty") 142 | var oldMutableProperty: String { 143 | get { 144 | mutableProperty 145 | } 146 | set { 147 | mutableProperty = newValue 148 | } 149 | } 150 | var computedImmutableProperty: String { 151 | _computedImmutableProperty 152 | } 153 | @available(*, deprecated, renamed: "computedImmutableProperty") 154 | var oldComputedImmutableProperty: String { 155 | computedImmutableProperty 156 | } 157 | 158 | private let _computedImmutableProperty: String 159 | var computedMutableProperty: String { 160 | get { 161 | _computedMutableProperty 162 | } 163 | set { 164 | _computedMutableProperty = newValue 165 | } 166 | } 167 | @available(*, deprecated, renamed: "computedMutableProperty") 168 | var oldComputedMutableProperty: String { 169 | get { 170 | computedMutableProperty 171 | } 172 | set { 173 | computedMutableProperty = newValue 174 | } 175 | } 176 | 177 | private var _computedMutableProperty: String 178 | 179 | init(immutableProperty: String, mutableProperty: String, computedImmutableProperty: String, computedMutableProperty: String) { 180 | self.immutableProperty = immutableProperty 181 | self.mutableProperty = mutableProperty 182 | _computedImmutableProperty = computedImmutableProperty 183 | _computedMutableProperty = computedMutableProperty 184 | } 185 | public func testFunction(_ unnamedParameter: String, argumentLabel parameterName: Int, previouslyUnnamed: Bool, trailingClosure: @escaping () -> Void) -> Bool { 186 | true 187 | } 188 | @available(*, deprecated, renamed: "testFunction(_:argumentLabel:previouslyUnnamed:trailingClosure:)") 189 | 190 | public func oldTestFunction(_ arg0: String, oldArgumentLabel arg1: Int, _ arg2: Bool, oldTrailingClosure arg3: @escaping () -> Void) -> Bool { 191 | testFunction(arg0, argumentLabel : arg1, previouslyUnnamed: arg2, trailingClosure: arg3) 192 | } 193 | func testFunctionWithoutReturn() { 194 | } 195 | @available(*, deprecated, renamed: "testFunctionWithoutReturn") 196 | func oldTestFunctionWithoutReturn() { 197 | testFunctionWithoutReturn() 198 | } 199 | } 200 | """, 201 | macros: testMacros 202 | ) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Sources/RenamedPlugin/Renamed.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxMacros 5 | 6 | public struct Renamed: PeerMacro { 7 | public static func expansion( 8 | of node: AttributeSyntax, 9 | providingPeersOf declaration: Declaration, 10 | in context: Context 11 | ) throws -> [DeclSyntax] { 12 | guard 13 | case .argumentList(let arguments) = node.argument, 14 | let firstElement = arguments.first, 15 | let stringLiteral = firstElement.expression.as(StringLiteralExprSyntax.self), 16 | stringLiteral.segments.count == 1, 17 | case .stringSegment(let previousName)? = stringLiteral.segments.first 18 | else { 19 | throw ErrorDiagnosticMessage(id: "missing-name-parameter", message: "'Renamed' requires a string literal containing the name of the old symbol") 20 | } 21 | 22 | if let structDecl = declaration.as(StructDeclSyntax.self) { 23 | return try expansion(of: node, providingPeersOf: structDecl, in: context, previousName: previousName.content.text) 24 | } else if let classDecl = declaration.as(ClassDeclSyntax.self) { 25 | return try expansion(of: node, providingPeersOf: classDecl, in: context, previousName: previousName.content.text) 26 | } else if let enumDecl = declaration.as(EnumDeclSyntax.self) { 27 | return try expansion(of: node, providingPeersOf: enumDecl, in: context, previousName: previousName.content.text) 28 | } else if let typealiasDecl = declaration.as(TypealiasDeclSyntax.self) { 29 | return try expansion(of: node, providingPeersOfTypealias: typealiasDecl, in: context, previousName: previousName.content) 30 | } else if let functionDecl = declaration.as(FunctionDeclSyntax.self) { 31 | return try expansion(of: node, providingPeersOf: functionDecl, in: context, previousName: previousName.content.text) 32 | } else if let variableDecl = declaration.as(VariableDeclSyntax.self) { 33 | return try expansion(of: node, providingPeersOf: variableDecl, in: context, previousName: previousName.content.text) 34 | } else { 35 | throw ErrorDiagnosticMessage(id: "invalid-declaration-type", message: "'Renamed' can only be applied to structs, classes, enums, typealiases, and variables") 36 | } 37 | } 38 | 39 | private static func expansion( 40 | of node: AttributeSyntax, 41 | providingPeersOf declaration: some IdentifiedDeclSyntax & ModifiedDeclSyntax, 42 | in context: Context, 43 | previousName: String 44 | ) throws -> [DeclSyntax] { 45 | let scope = ({ 46 | for modifier in declaration.modifiers ?? [] { 47 | switch (modifier.name.tokenKind) { 48 | case .keyword(.public): 49 | return "public " 50 | case .keyword(.internal): 51 | return "internal " 52 | case .keyword(.fileprivate): 53 | return "fileprivate " 54 | case .keyword(.private): 55 | return "private " 56 | default: 57 | break 58 | } 59 | } 60 | 61 | return "" 62 | })() 63 | 64 | return [ 65 | """ 66 | @available(*, deprecated, renamed: "\(raw: declaration.identifier.text)") 67 | \(raw: scope)typealias \(raw: previousName) = \(raw: declaration.identifier.text) 68 | """ 69 | ] 70 | } 71 | 72 | private static func expansion( 73 | of node: AttributeSyntax, 74 | providingPeersOfTypealias declaration: TypealiasDeclSyntax, 75 | in context: Context, 76 | previousName: TokenSyntax 77 | ) throws -> [DeclSyntax] { 78 | let defaultVersionRestriction = AvailabilityVersionRestrictionSyntax( 79 | platform: .identifier("*") 80 | ) 81 | 82 | let versionRestriction: AvailabilityVersionRestrictionSyntax = declaration.attributes?.lazy.compactMap { attribute -> AvailabilityVersionRestrictionSyntax? in 83 | attribute.as(AttributeSyntax.self)?.argument?.as(AvailabilitySpecListSyntax.self)?.lazy.compactMap { argument -> AvailabilityVersionRestrictionSyntax? in 84 | argument.entry.as(AvailabilityVersionRestrictionSyntax.self) 85 | }.first 86 | }.first ?? defaultVersionRestriction 87 | 88 | let availabilityArgument = AvailabilitySpecListSyntax { 89 | AvailabilityArgumentSyntax( 90 | entry: .availabilityVersionRestriction(versionRestriction), 91 | trailingComma: .commaToken() 92 | ) 93 | AvailabilityArgumentSyntax( 94 | entry: .token(.keyword(.deprecated)), 95 | trailingComma: .commaToken() 96 | ) 97 | AvailabilityArgumentSyntax( 98 | entry: .availabilityLabeledArgument( 99 | AvailabilityLabeledArgumentSyntax(label: .keyword(.renamed), value: .string(StringLiteralExprSyntax(content: declaration.identifier.text))) 100 | ) 101 | ) 102 | } 103 | 104 | let availableAttribute = AttributeSyntax( 105 | attributeName: SimpleTypeIdentifierSyntax( 106 | name: .keyword(.available) 107 | ), 108 | leftParen: .leftParenToken(), 109 | argument: .availability(availabilityArgument), 110 | rightParen: .rightParenToken() 111 | ) 112 | 113 | var identifier = previousName 114 | identifier.leadingTrivia = .space 115 | 116 | let typealiasDecl = TypealiasDeclSyntax( 117 | modifiers: declaration.modifiers, 118 | identifier: identifier, 119 | genericParameterClause: declaration.genericParameterClause, 120 | initializer: TypeInitializerClauseSyntax(value: SimpleTypeIdentifierSyntax(name: .identifier(declaration.identifier.text))), 121 | genericWhereClause: declaration.genericWhereClause 122 | ) 123 | 124 | return [ 125 | """ 126 | \(availableAttribute)\(typealiasDecl) 127 | """ 128 | ] 129 | } 130 | 131 | private static func expansion( 132 | of node: AttributeSyntax, 133 | providingPeersOf declaration: FunctionDeclSyntax, 134 | in context: Context, 135 | previousName: String 136 | ) throws -> [DeclSyntax] { 137 | let scope: DeclModifierSyntax? = ({ 138 | for modifier in declaration.modifiers ?? [] { 139 | switch (modifier.name.tokenKind) { 140 | case .keyword(.public): 141 | return modifier 142 | case .keyword(.internal): 143 | return modifier 144 | case .keyword(.fileprivate): 145 | return modifier 146 | case .keyword(.private): 147 | return modifier 148 | default: 149 | break 150 | } 151 | } 152 | 153 | return nil 154 | })() 155 | 156 | let functionNameSplit = previousName.split(separator: "(", maxSplits: 1) 157 | 158 | let newParameters: String 159 | 160 | let argumentsAndTypes = declaration.signature.input.parameterList.map { parameter -> (TokenSyntax?, TypeSyntaxProtocol) in 161 | let type = parameter.type 162 | 163 | if parameter.firstName.text == "_" { 164 | return (nil, type) 165 | } else { 166 | return (parameter.firstName, type) 167 | } 168 | } 169 | 170 | let parametersInBody = argumentsAndTypes.map(\.0).enumerated().map { index, argumentName in 171 | if let argumentName { 172 | return "\(argumentName): arg\(index)" 173 | } else { 174 | return "arg\(index)" 175 | } 176 | }.joined(separator: ", ") 177 | let body: DeclSyntax = "\(declaration.identifier)(\(raw: parametersInBody))" 178 | 179 | if functionNameSplit.count == 2 { 180 | guard functionNameSplit[1].last == ")" else { 181 | throw ErrorDiagnosticMessage(id: "invalid-function-signature", message: "The provided function signature is not valid. Missing matching closing parenthesis.") 182 | } 183 | 184 | let parametersSplit = functionNameSplit[1].dropLast().split(separator: ":") 185 | 186 | guard declaration.signature.input.parameterList.count == parametersSplit.count else { 187 | throw ErrorDiagnosticMessage(id: "invalid-function-parameter-count", message: "The old function signature must have the same number of parameters as the function 'Renamed' is applied to.") 188 | } 189 | 190 | newParameters = zip(parametersSplit, argumentsAndTypes.map(\.1)).enumerated().map { index, argumentLabelAndType in 191 | "\(argumentLabelAndType.0) arg\(index): \(argumentLabelAndType.1)" 192 | }.joined(separator: ", ") 193 | } else { 194 | guard declaration.signature.input.parameterList.count == 0 else { 195 | throw ErrorDiagnosticMessage(id: "invalid-function-parameter-count", message: "The old function signature must have the same number of parameters as the function 'Renamed' is applied to.") 196 | } 197 | 198 | newParameters = "" 199 | } 200 | 201 | var renamedParameters = declaration.signature.input.parameterList.map { $0.firstName.text }.joined(separator: ":") 202 | if !renamedParameters.isEmpty { 203 | renamedParameters = "(" + renamedParameters + ":)" 204 | } 205 | 206 | let functionName = functionNameSplit[0] 207 | let signature: DeclSyntax = "\(raw: functionName)(\(raw: newParameters)) \(optional: declaration.signature.output)" 208 | 209 | return [ 210 | """ 211 | @available(*, deprecated, renamed: "\(declaration.identifier)\(raw: renamedParameters)") 212 | \(optional: scope)func \(signature){ 213 | \(body) 214 | } 215 | """ 216 | ] 217 | } 218 | 219 | private static func expansion( 220 | of node: AttributeSyntax, 221 | providingPeersOf declaration: VariableDeclSyntax, 222 | in context: Context, 223 | previousName: String 224 | ) throws -> [DeclSyntax] { 225 | guard 226 | let binding = declaration.bindings.first, 227 | let propertyName = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text, 228 | let type = binding.typeAnnotation?.type.as(SimpleTypeIdentifierSyntax.self)?.name 229 | else { 230 | // Variables declared with some values, e.g. `var test = 1`, will have an implicit type. These could be supported by looking a 231 | // initialiser but it gets complex once basic types are done, e.g. to infer the type of an array. 232 | throw ErrorDiagnosticMessage(id: "missing-variable-type", message: "'Renamed' requires a variable to be explicitly typed") 233 | } 234 | 235 | let scope = ({ 236 | for modifier in declaration.modifiers ?? [] { 237 | switch (modifier.name.tokenKind) { 238 | case .keyword(.public): 239 | return "public " 240 | case .keyword(.internal): 241 | return "internal " 242 | case .keyword(.fileprivate): 243 | return "fileprivate " 244 | case .keyword(.private): 245 | return "private " 246 | default: 247 | break 248 | } 249 | } 250 | 251 | return "" 252 | })() 253 | 254 | lazy var immutableProperty: DeclSyntax = """ 255 | @available(*, deprecated, renamed: "\(raw: propertyName)") 256 | \(raw: scope)var \(raw: previousName): \(raw: type) { 257 | \(raw: propertyName) 258 | } 259 | """ 260 | 261 | lazy var mutableProperty: DeclSyntax = """ 262 | @available(*, deprecated, renamed: "\(raw: propertyName)") 263 | \(raw: scope)var \(raw: previousName): \(raw: type) { 264 | get { 265 | \(raw: propertyName) 266 | } 267 | set { 268 | \(raw: propertyName) = newValue 269 | } 270 | } 271 | """ 272 | 273 | switch declaration.bindingKeyword.tokenKind { 274 | case .keyword(.let): 275 | return [ 276 | immutableProperty 277 | ] 278 | case .keyword(.var): 279 | guard let binding = declaration.bindings.first else { 280 | throw ErrorDiagnosticMessage(id: "missing-binding", message: "'Renamed' is only supported on variables with at least 1 binding") 281 | } 282 | 283 | if let accessor = binding.accessor { 284 | // Could have get/set/_modify 285 | if accessor.is(CodeBlockSyntax.self) { 286 | // This is a "naked" getter, e.g. not `get` or `set` 287 | return [immutableProperty] 288 | } 289 | 290 | guard let accessor = accessor.as(AccessorBlockSyntax.self) else { 291 | throw ErrorDiagnosticMessage(id: "unsupported-block", message: "'Renamed' is only supported on variables with block and explicit accessor syntax") 292 | } 293 | 294 | // TODO: Possible support other accessors, e.g. `_modify`, `willSet`, and `didSet` 295 | 296 | guard accessor.accessors.contains(where: { $0.accessorKind.tokenKind == .keyword(.get) }) else { 297 | throw ErrorDiagnosticMessage(id: "missing-get-accessor", message: "'Renamed' is only supported on variables with a getter") 298 | } 299 | 300 | if accessor.accessors.contains(where: { $0.accessorKind.tokenKind == .keyword(.set) }) { 301 | return [mutableProperty] 302 | } else { 303 | return [immutableProperty] 304 | } 305 | } else { 306 | // Not accessor; just a plain `var`. 307 | return [mutableProperty] 308 | } 309 | default: 310 | throw ErrorDiagnosticMessage(id: "unsupported-variable", message: "'Renamed' is only supported on var and let variables. This is a \(declaration.bindingKeyword)") 311 | } 312 | } 313 | } 314 | 315 | private struct InvalidDeclarationTypeError: Error {} 316 | 317 | private struct ErrorDiagnosticMessage: DiagnosticMessage, Error { 318 | let message: String 319 | let diagnosticID: MessageID 320 | let severity: DiagnosticSeverity 321 | 322 | init(id: String, message: String) { 323 | self.message = message 324 | diagnosticID = MessageID(domain: "uk.josephduffy.Renamed", id: id) 325 | severity = .error 326 | } 327 | } 328 | 329 | private protocol ModifiedDeclSyntax: SyntaxProtocol { 330 | var modifiers: ModifierListSyntax? { get set } 331 | } 332 | 333 | extension StructDeclSyntax: ModifiedDeclSyntax {} 334 | extension ClassDeclSyntax: ModifiedDeclSyntax {} 335 | extension EnumDeclSyntax: ModifiedDeclSyntax {} 336 | extension TypealiasDeclSyntax: ModifiedDeclSyntax {} 337 | --------------------------------------------------------------------------------