Skip to content

Commit

Permalink
Merge pull request #19 from p-x9/feature/enum-case-alias
Browse files Browse the repository at this point in the history
Enum case alias
  • Loading branch information
p-x9 authored Nov 7, 2023
2 parents 8456506 + 9b81df7 commit 162cd53
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 23 deletions.
100 changes: 83 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# AliasMacro

Swift Macro for defining aliases for types, functions, or variables.

<!-- # Badges -->
Expand All @@ -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")
Expand All @@ -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("こんにちは")
Expand All @@ -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)
Expand All @@ -100,25 +92,99 @@ 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

@Alias("ultimate")
case master(level: Int)
}
```

At this time, the macro is expanded as follows.

```swift
enum Difficulty {
case easy
case medium
case hard
case expert
case master(level: Int)

static let beginner: Self = .easy
static let normal: Self = .medium
static let challenge: Self = .hard
static let extreme: Self = .expert

static func ultimate(level: Int) -> Self {
.master(level)
}
}
```

### 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("こんにちは")
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)
89 changes: 84 additions & 5 deletions Sources/AliasPlugin/AliasMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: ":")
Expand All @@ -31,7 +31,7 @@ struct AliasMacro {
} else {
self.functionArgumentLabels = []
}
self.accessControl = accessControl ?? .inherit
self.accessControl = accessControl
}
}

Expand Down Expand Up @@ -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 []
}
Expand All @@ -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)"

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -192,4 +199,76 @@ 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 []
}

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 {
return [
"\(raw: accessModifier) \(aliasDecl)"
]
}
return [
aliasDecl
]
}
}
}
10 changes: 10 additions & 0 deletions Sources/AliasPlugin/AliasMacroDiagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum AliasMacroDiagnostic {
case unsupportedDeclaration
case specifyTypeExplicitly
case multipleVariableDeclarationIsNotSupported
case enumCaseCannotInheritAccessModifiers
case multipleEnumCaseDeclarationIsNotSupported
}

extension AliasMacroDiagnostic: DiagnosticMessage {
Expand All @@ -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.
"""
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/AliasSupport/Extesnsion/AttributeListSyntax+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ extension AttributeListSyntax {
}
return true
}
return .init(attributes)
return attributes
}
}
42 changes: 42 additions & 0 deletions Sources/AliasSupport/Extesnsion/EnumCaseParameterListSyntax+.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 162cd53

Please sign in to comment.