From 7543dfd8b705bea9677ec734ede8f1e8f5ac6dd1 Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 06:57:15 +0900 Subject: [PATCH 1/7] support alias of enum case --- Sources/AliasPlugin/AliasMacro.swift | 49 +++++++++++++++++-- .../AliasPlugin/AliasMacroDiagnostic.swift | 10 ++++ .../Extesnsion/AttributeListSyntax+.swift | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/Sources/AliasPlugin/AliasMacro.swift b/Sources/AliasPlugin/AliasMacro.swift index 7e881ba..1765845 100644 --- a/Sources/AliasPlugin/AliasMacro.swift +++ b/Sources/AliasPlugin/AliasMacro.swift @@ -21,7 +21,7 @@ struct AliasMacro { struct Arguments { let alias: String let functionArgumentLabels: [String] - let accessControl: AccessControl + let accessControl: AccessControl? init(alias: String, accessControl: AccessControl?) { let components = alias.components(separatedBy: ":") @@ -31,7 +31,7 @@ struct AliasMacro { } else { self.functionArgumentLabels = [] } - self.accessControl = accessControl ?? .inherit + self.accessControl = accessControl } } @@ -81,6 +81,13 @@ extension AliasMacro: PeerMacro { attribute: node) } + if let enumCase = declaration.as(EnumCaseDeclSyntax.self) { + return enumCaseAlias(for: enumCase, + with: arguments, + attribute: node, + in: context) + } + context.diagnose(AliasMacroDiagnostic.unsupportedDeclaration.diagnose(at: node)) return [] } @@ -91,7 +98,7 @@ extension AliasMacro { with arguments: Arguments, attribute: AttributeSyntax) -> [DeclSyntax] { - let accessModifier = arguments.accessControl.modifier ?? typeDecl.accessModifier + let accessModifier = arguments.accessControl?.modifier ?? typeDecl.accessModifier let alias: DeclSyntax = "typealias \(raw: arguments.alias) = \(typeDecl.identifier.trimmed)" @@ -122,7 +129,7 @@ extension AliasMacro { } let attributes = varDecl.attributes.removed(attribute) - let accessModifier = arguments.accessControl.modifier ?? varDecl.accessModifier + let accessModifier = arguments.accessControl?.modifier ?? varDecl.accessModifier return [ .init( @@ -157,7 +164,7 @@ extension AliasMacro { let baseIdentifier: TokenSyntax = isInstance ? .keyword(.`self`) : .keyword(.Self) let attributes = functionDecl.attributes.removed(attribute) - let accessModifier = arguments.accessControl.modifier ?? functionDecl.accessModifier + let accessModifier = arguments.accessControl?.modifier ?? functionDecl.accessModifier let parameters: [FunctionParameterSyntax] = functionDecl.signature.parameterClause.parameters.enumerated() .map { i, param in @@ -192,4 +199,36 @@ extension AliasMacro { .init(newDecl) ] } + + static func enumCaseAlias(for caseDecl: EnumCaseDeclSyntax, + with arguments: Arguments, + attribute: AttributeSyntax, + in context: MacroExpansionContext) -> [DeclSyntax] { + if arguments.accessControl == .inherit { + context.diagnose( + AliasMacroDiagnostic.enumCaseCannotInheritAccessModifiers.diagnose(at: attribute) + ) + return [] + } + if caseDecl.elements.count > 1 { + context.diagnose( + AliasMacroDiagnostic.multipleEnumCaseDeclarationIsNotSupported.diagnose(at: caseDecl.elements) + ) + return [] + } + guard let element = caseDecl.elements.first else { + return [] + } + + let aliasDecl: DeclSyntax = "static let \(raw: arguments.alias): Self = .\(element.name)" + + if let accessModifier = arguments.accessControl?.modifier { + return [ + "\(raw: accessModifier) \(aliasDecl)" + ] + } + return [ + aliasDecl + ] + } } diff --git a/Sources/AliasPlugin/AliasMacroDiagnostic.swift b/Sources/AliasPlugin/AliasMacroDiagnostic.swift index 3c3e7fc..d240978 100644 --- a/Sources/AliasPlugin/AliasMacroDiagnostic.swift +++ b/Sources/AliasPlugin/AliasMacroDiagnostic.swift @@ -13,6 +13,8 @@ public enum AliasMacroDiagnostic { case unsupportedDeclaration case specifyTypeExplicitly case multipleVariableDeclarationIsNotSupported + case enumCaseCannotInheritAccessModifiers + case multipleEnumCaseDeclarationIsNotSupported } extension AliasMacroDiagnostic: DiagnosticMessage { @@ -30,6 +32,14 @@ extension AliasMacroDiagnostic: DiagnosticMessage { return """ Multiple variable declaration in one statement is not supported. """ + case .enumCaseCannotInheritAccessModifiers: + return """ + Enum case has no access modifier and cannot inherit. + """ + case .multipleEnumCaseDeclarationIsNotSupported: + return """ + Multiple enum case declaration in one statement is not supported. + """ } } diff --git a/Sources/AliasSupport/Extesnsion/AttributeListSyntax+.swift b/Sources/AliasSupport/Extesnsion/AttributeListSyntax+.swift index f556f2c..54ff41a 100644 --- a/Sources/AliasSupport/Extesnsion/AttributeListSyntax+.swift +++ b/Sources/AliasSupport/Extesnsion/AttributeListSyntax+.swift @@ -17,6 +17,6 @@ extension AttributeListSyntax { } return true } - return .init(attributes) + return attributes } } From 6e7051d03ef3a2fda1ee13cdb34ffea360d02643 Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 06:58:07 +0900 Subject: [PATCH 2/7] add test case for enum case alias --- Tests/AliasTests/AliasTests.swift | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/Tests/AliasTests/AliasTests.swift b/Tests/AliasTests/AliasTests.swift index 32a01c2..a39a50f 100644 --- a/Tests/AliasTests/AliasTests.swift +++ b/Tests/AliasTests/AliasTests.swift @@ -192,6 +192,46 @@ final class AliasTests: XCTestCase { ) } + func testEnumCaseAlias() throws { + assertMacroExpansion( + """ + enum ItemType { + @Alias("HEAD") + case header + } + """, + expandedSource: + """ + enum ItemType { + case header + + static let HEAD: Self = .header + } + """, + macros: macros + ) + } + + func testEnumCaseAliasWithAccessModifier() throws { + assertMacroExpansion( + """ + enum ItemType { + @Alias("HEAD", access: .public) + case header + } + """, + expandedSource: + """ + enum ItemType { + case header + + public static let HEAD: Self = .header + } + """, + macros: macros + ) + } + func testMultipleTypeAlias() throws { assertMacroExpansion( """ @@ -278,4 +318,58 @@ final class AliasTests: XCTestCase { macros: macros ) } + + func testDiagnosticsEnumCaseInheritAccessModifier() throws { + assertMacroExpansion( + """ + enum ItemType { + @Alias("HEAD", access: .inherit) + case header + } + """, + expandedSource: + """ + enum ItemType { + case header + } + """, + diagnostics: [ + DiagnosticSpec( + message: AliasMacroDiagnostic + .enumCaseCannotInheritAccessModifiers + .message, + line: 2, + column: 4 + ) + ], + macros: macros + ) + } + + func testDiagnosticsMultipleEnumCase() throws { + assertMacroExpansion( + """ + enum ItemType { + @Alias("HEAD", access: .public) + case header, footer + } + """, + expandedSource: + """ + enum ItemType { + case header, footer + } + """, + diagnostics: [ + DiagnosticSpec( + message: AliasMacroDiagnostic + .multipleEnumCaseDeclarationIsNotSupported + .message, + line: 3, + column: 9 + ) + ], + macros: macros + ) + } } From be7d50251e4855ecd515a994e383494c7d8b187b Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:01:20 +0900 Subject: [PATCH 3/7] support for enum case with parametors --- Sources/AliasPlugin/AliasMacro.swift | 52 ++++++++++++++++--- .../EnumCaseParameterListSyntax+.swift | 42 +++++++++++++++ Tests/AliasTests/AliasTests.swift | 22 ++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 Sources/AliasSupport/Extesnsion/EnumCaseParameterListSyntax+.swift diff --git a/Sources/AliasPlugin/AliasMacro.swift b/Sources/AliasPlugin/AliasMacro.swift index 1765845..cdb5bcc 100644 --- a/Sources/AliasPlugin/AliasMacro.swift +++ b/Sources/AliasPlugin/AliasMacro.swift @@ -220,15 +220,55 @@ extension AliasMacro { return [] } - let aliasDecl: DeclSyntax = "static let \(raw: arguments.alias): Self = .\(element.name)" + if let parameterClause = element.parameterClause { +// var argNames: [TokenSyntax] = [] +// let parameters = parameterClause.parameters +// .enumerated() +// .map { +// if let firstName = $1.firstName, $1.secondName == nil { +// argNames.append(firstName) +// return "\(firstName.trimmed): \($1.type)" +// } else if let firstName = $1.firstName, let secondName = $1.secondName { +// argNames.append(secondName) +// return "\(firstName.trimmed) \(secondName.trimmed): \($1.type)" +// } else { +// argNames.append(.identifier("arg\($0)")) +// return "_ arg\($0): \($1.type.trimmed)" +// } +// } +// .joined(separator: ", ") + let parameters = parameterClause.parameters + let functionParameters = parameters.functionParameterListSyntax.map { + "\($0)" + }.joined(separator: ", ") + let functionArguments = parameters.labeledExprListSyntax.map { + "\($0)" + }.joined(separator: ", ") + + let aliasDecl: DeclSyntax = """ + static func \(raw: arguments.alias)(\(raw: functionParameters)) -> Self { + .\(element.name)(\(raw: functionArguments)) + } + """ + if let accessModifier = arguments.accessControl?.modifier { + return [ + "\(raw: accessModifier) \(aliasDecl)" + ] + } + return [ + aliasDecl + ] + } else { + let aliasDecl: DeclSyntax = "static let \(raw: arguments.alias): Self = .\(element.name)" - if let accessModifier = arguments.accessControl?.modifier { + if let accessModifier = arguments.accessControl?.modifier { + return [ + "\(raw: accessModifier) \(aliasDecl)" + ] + } return [ - "\(raw: accessModifier) \(aliasDecl)" + aliasDecl ] } - return [ - aliasDecl - ] } } diff --git a/Sources/AliasSupport/Extesnsion/EnumCaseParameterListSyntax+.swift b/Sources/AliasSupport/Extesnsion/EnumCaseParameterListSyntax+.swift new file mode 100644 index 0000000..179c25a --- /dev/null +++ b/Sources/AliasSupport/Extesnsion/EnumCaseParameterListSyntax+.swift @@ -0,0 +1,42 @@ +// +// EnumCaseParameterListSyntax+.swift +// +// +// Created by p-x9 on 2023/11/07. +// +// + +import SwiftSyntax +import SwiftSyntaxBuilder + +extension EnumCaseParameterListSyntax { + package var functionParameterListSyntax: FunctionParameterListSyntax { + let parameters: [FunctionParameterSyntax] = self.enumerated() + .map { + if let firstName = $1.firstName, $1.secondName == nil { + return "\(firstName.trimmed): \($1.type)" + } else if let firstName = $1.firstName, let secondName = $1.secondName { + return "\(firstName.trimmed) \(secondName.trimmed): \($1.type)" + } else { + return "_ arg\(raw: $0): \($1.type.trimmed)" + } + } + return .init(parameters) + } + + package var labeledExprListSyntax: LabeledExprListSyntax { + let arguments: [LabeledExprSyntax] = self.enumerated() + .map { + if let firstName = $1.firstName, $1.secondName == nil { + return .init(label: "\(firstName.trimmed)", + expression: ExprSyntax("\(firstName.trimmed)")) + } else if let firstName = $1.firstName, let secondName = $1.secondName { + return .init(label: "\(firstName.trimmed)", + expression: ExprSyntax("\(secondName.trimmed)")) + } else { + return .init(expression: ExprSyntax("arg\(raw: $0)")) + } + } + return .init(arguments) + } +} diff --git a/Tests/AliasTests/AliasTests.swift b/Tests/AliasTests/AliasTests.swift index a39a50f..f84685c 100644 --- a/Tests/AliasTests/AliasTests.swift +++ b/Tests/AliasTests/AliasTests.swift @@ -212,6 +212,28 @@ final class AliasTests: XCTestCase { ) } + func testEnumCaseAliasWithParameters() throws { + assertMacroExpansion( + """ + enum ItemType { + @Alias("HEAD") + case header(String, subtitle: String, index: Int) + } + """, + expandedSource: + """ + enum ItemType { + case header(String, subtitle: String, index: Int) + + static func HEAD(_ arg0: String, subtitle: String, index: Int) -> Self { + .header(arg0, subtitle: subtitle, index: index) + } + } + """, + macros: macros + ) + } + func testEnumCaseAliasWithAccessModifier() throws { assertMacroExpansion( """ From e1b15b5291a3eeb22b1d22a9c85f2ec712c4b31c Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:01:43 +0900 Subject: [PATCH 4/7] update README.md for enum case --- README.md | 92 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7390591..66f9300 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # AliasMacro + Swift Macro for defining aliases for types, functions, or variables. @@ -9,25 +10,13 @@ Swift Macro for defining aliases for types, functions, or variables. [![Github top language](https://img.shields.io/github/languages/top/p-x9/AliasMacro)](https://github.com/p-x9/AliasMacro/) ## Usage -A macro that can be attached to a type, function or variable. - -### Class/Struct/Enum/Actor -For example, the `ViewController` can also be referenced as a `VC` by writing the following. -(If this macro is used for type, it is defined internally simply using `typealias`.) -```swift -@Alias("VC") -class ViewContoroller: UIViewController { - /* --- */ -} - -/* ↓↓↓↓↓ */ -print(ViewContoroller.self) // => "ViewController" -print(VC.self) // => "ViewController" -``` +A macro that can be attached to a type, function or variable. ### Variable + You can define an alias for a variable as follows + ```swift class SomeClass { @Alias("title") @@ -43,8 +32,10 @@ print(title) // => "hello" ``` ### Function/Method + You can define an alias for a function as follows. In this way, you can call both `hello("aaa", at: Date())` and `こんにちは("aaa", at: Date())`. + ```swift class SomeClass { @Alias("こんにちは") @@ -63,17 +54,18 @@ someClass.こんにちは("aaa", at: Date()) // => "aaa" ``` #### Customize Argument Label + Argument labels can also be customized by separating them with ":". ```swift class SomeClass { - @Alias("こんにちは:いつ") + @Alias("こんにちは::いつ") func hello(_ text: String, at date: Date) { /* --- */ print(text) } - @Alias("こんにちは2:_") // Omit argument labels + @Alias("こんにちは2:_:_") // Omit argument labels func hello2(_ text: String, at date: Date) { /* --- */ print(text) @@ -100,8 +92,69 @@ someClass.hello3("aaa", at: Date(), to: "you") // => "aaa" someClass.こんにちは3("aaa", at: Date(), 宛: "あなた") // => "aaa" ``` +### Enum Case + +You can define alias for enum case. + +For example, suppose we define the following. + +```swift +enum Difficulty { + @Alias("beginner") + case easy + + @Alias("normal") + case medium + + @Alias("challenge") + case hard + + @Alias("extreme") + case expert +} +``` + +At this time, the macro is expanded as follows. + +```swift +enum Difficulty { + case easy + case medium + case hard + case expert + + static let beginner: Self = .easy + static let normal: Self = .medium + static let challenge: Self = .hard + static let extreme: Self = .expert +} +``` + +### Class/Struct/Enum/Actor + +For example, the `ViewController` can also be referenced as a `VC` by writing the following. +(If this macro is used for type, it is defined internally simply using `typealias`.) + +> **Warning** +> `PeerMacro` with arbitrary specified in `names` cannot be used in global scope. +> [Restrictions on arbitrary names](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#restrictions-on-arbitrary-names) + +```swift +@Alias("VC") +class ViewContoroller: UIViewController { + /* --- */ +} + +/* ↓↓↓↓↓ */ + +print(ViewContoroller.self) // => "ViewController" +print(VC.self) // => "ViewController" +``` + ### Multiple Aliases + Multiple aliases can be defined by adding multiple macros as follows + ```swift @Alias("hello") @Alias("こんにちは") @@ -109,16 +162,21 @@ var text: String = "Hello" ``` ### Specify Access Modifier of Alias + You can specify an alias access modifier as follows. + ```swift @Alias("hello", access: .public) private var text: String = "Hello" ``` If set "inherit", it will inherit from the original definition. + ```swift @Alias("hello", access: .inherit) private var text: String = "Hello" ``` + ## License + AliasMacro is released under the MIT License. See [LICENSE](./LICENSE) From d1ddd9caa4405f550996399a6428adc8dfc6a736 Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:07:00 +0900 Subject: [PATCH 5/7] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 66f9300..4a74e0f 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ enum Difficulty { @Alias("extreme") case expert + + @Alias("ultimate") + case master(level: Int) } ``` @@ -127,6 +130,10 @@ enum Difficulty { static let normal: Self = .medium static let challenge: Self = .hard static let extreme: Self = .expert + + static func ultimate(level: Int) -> Self { + .master(level) + } } ``` From b3d0bb6f8b9c938bd425f494fea9593fb24a82a2 Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:07:22 +0900 Subject: [PATCH 6/7] fix --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a74e0f..7745343 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ enum Difficulty { case medium case hard case expert + case master static let beginner: Self = .easy static let normal: Self = .medium From 9b81df7348e98e938d58da21e176d6c7944a642f Mon Sep 17 00:00:00 2001 From: p-x9 <50244599+p-x9@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:00:39 +0900 Subject: [PATCH 7/7] fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7745343..bbcee7e 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ enum Difficulty { case medium case hard case expert - case master + case master(level: Int) static let beginner: Self = .easy static let normal: Self = .medium