Skip to content

Commit da3ff4f

Browse files
committed
Initial commit
1 parent c2ce16e commit da3ff4f

20 files changed

+1204
-0
lines changed

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"pins" : [
3+
{
4+
"identity" : "swift-syntax",
5+
"kind" : "remoteSourceControl",
6+
"location" : "https://github.com/apple/swift-syntax.git",
7+
"state" : {
8+
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5",
9+
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"
10+
}
11+
}
12+
],
13+
"version" : 2
14+
}

Package.swift

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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: "ExtractCaseValue",
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: "ExtractCaseValue",
14+
targets: ["ExtractCaseValue"]
15+
),
16+
.executable(
17+
name: "ExtractCaseValueClient",
18+
targets: ["ExtractCaseValueClient"]
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: "ExtractCaseValueMacros",
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: "ExtractCaseValue", dependencies: ["ExtractCaseValueMacros"]),
39+
40+
// A client of the library, which is able to use the macro in its own code.
41+
.executableTarget(name: "ExtractCaseValueClient", dependencies: ["ExtractCaseValue"]),
42+
43+
// A test target used to develop the macro implementation.
44+
.testTarget(
45+
name: "ExtractCaseValueTests",
46+
dependencies: [
47+
"ExtractCaseValueMacros",
48+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
49+
]
50+
),
51+
]
52+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ExtractCaseValue
2+
3+
The `ExtractCaseValue` package provides a macro to expose assiocated values from enum cases as a computed property.
4+
5+
@Metadata {
6+
@PageColor(blue)
7+
}
8+
9+
## Overview
10+
11+
@Row {
12+
@Column {
13+
To extract a simple value annotate an enum with the `ExtractCaseValue` macro and provide the expected type as a generic along with a name for the comuted property. This will use the `firstMatchingType` as a default to use the first associated value in a case that matches the expected type (in this case `String`).
14+
}
15+
@Column {
16+
![Screenshot of Xcode showing the marco expansion on a Path enum with a String as return type](sample-one)
17+
}
18+
}
19+
20+
@Row {
21+
@Column {
22+
If the return type is optional the macro will infer `nil` as the default value.
23+
}
24+
@Column {
25+
![Screenshot of Xcode showing the marco expansion on a JSON enum with an optional String as return type](sample-two)
26+
}
27+
}
28+
29+
@Row {
30+
@Column {
31+
![Screenshot of Xcode showing the fix-it](fix-it)
32+
}
33+
@Column {
34+
Otherwise, you will get a fix-it that recommends to use a default value.
35+
}
36+
}
37+
38+
@Row {
39+
@Column {
40+
You can also add mutliple `ExtractCaseValue` macros.
41+
![Screenshot of Xcode showing the marco expansion on a Coordinate enum which uses multiple macros](sample-three)
42+
}
43+
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import ExtractCaseValueMacros
2+
3+
/// A macro that extracts an associated value from enum cases using a default value if
4+
/// extraction is not possible.
5+
///
6+
/// For exampleDiesr codu
7+
///
8+
/// ```swift
9+
/// @ExtractCaseValue<String>(name: "path", kind: CaseExtractionKind.position(0), defaultValue: "")
10+
/// enum Path {
11+
/// case relative(String)
12+
/// case absolute(String)
13+
/// case root
14+
/// }
15+
/// ```
16+
/// produces
17+
///
18+
/// ```swift
19+
/// enum Path {
20+
/// case relative(String)
21+
/// case absolute(String)
22+
/// case root
23+
/// var path: String {
24+
/// switch self {
25+
/// case let .relative(__macro_local_4pathfMu_):
26+
/// return __macro_local_4pathfMu_
27+
/// case let .absolute(__macro_local_4pathfMu0_):
28+
/// return __macro_local_4pathfMu0_
29+
/// case .root:
30+
/// return ""
31+
/// }
32+
/// }
33+
/// }
34+
/// ```
35+
@attached(member, names: arbitrary)
36+
public macro ExtractCaseValue<T>(name: String, kind: CaseExtractionKind = .default, defaultValue: T) = #externalMacro(module: "ExtractCaseValueMacros", type: "ExtractCaseValueMacro")
37+
38+
/// A macro that extracts an associated value from enum cases.
39+
///
40+
/// For example
41+
///
42+
/// ```swift
43+
/// @ExtractCaseValue<String>(name: "path", kind: CaseExtractionKind.position(0))
44+
/// enum Path {
45+
/// case relative(String)
46+
/// case absolute(String)
47+
/// }
48+
/// ```
49+
/// produces
50+
///
51+
/// ```swift
52+
/// enum Path {
53+
/// case relative(String)
54+
/// case absolute(String)
55+
/// case root
56+
/// var path: String {
57+
/// switch self {
58+
/// case let .relative(__macro_local_4pathfMu_):
59+
/// return __macro_local_4pathfMu_
60+
/// case let .absolute(__macro_local_4pathfMu0_):
61+
/// return __macro_local_4pathfMu0_
62+
/// }
63+
/// }
64+
/// }
65+
/// ```
66+
@attached(member, names: arbitrary)
67+
public macro ExtractCaseValue<T>(name: String, kind: CaseExtractionKind = .default) = #externalMacro(module: "ExtractCaseValueMacros", type: "ExtractCaseValueMacro")
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ExtractCaseValue
2+
3+
@ExtractCaseValue<Double>(name: "x", kind: .associatedValueName("x"))
4+
@ExtractCaseValue<Double>(name: "y", kind: .associatedValueName("y"))
5+
@ExtractCaseValue<Double?>(name: "z", kind: .associatedValueName("z"), defaultValue: nil)
6+
enum Coordinate {
7+
case twoDee(x: Double, y: Double)
8+
case threeDee(x: Double, y: Double, z: Double)
9+
}
10+
11+
@ExtractCaseValue<String?>(name: "string", kind: .firstMatchingType)
12+
enum JSON {
13+
case string(String)
14+
case number(Double)
15+
case object([String: JSON])
16+
case array([JSON])
17+
case bool(Bool)
18+
case null
19+
}
20+
21+
@ExtractCaseValue<String>(name: "path")
22+
enum Path {
23+
case absolute(String)
24+
case relative(String)
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftSyntax
2+
3+
/// The available kinds of case value extractions.
4+
public enum CaseExtractionKind {
5+
/// Extract a value at a position in the associated values.
6+
case position(Int)
7+
8+
/// Extract a value with a certain name.
9+
case associatedValueName(String)
10+
11+
/// Extract the first value with a matching type.
12+
case firstMatchingType
13+
14+
public static let `default` = Self.firstMatchingType
15+
}
16+
17+
extension CaseExtractionKind {
18+
init?(expr: ExprSyntax) {
19+
guard
20+
let functionCall = expr.as(FunctionCallExprSyntax.self),
21+
let memberAccessExpr = functionCall.calledExpression.as(MemberAccessExprSyntax.self)
22+
else { return nil }
23+
24+
let firstIntArgument = (functionCall.argumentList.first?.expression.as(IntegerLiteralExprSyntax.self)?.digits.text).flatMap(Int.init)
25+
let firstStringArgument = functionCall.argumentList.first?.expression.stringLiteralSegment
26+
27+
switch memberAccessExpr.name.text {
28+
case "position" :
29+
guard let position = firstIntArgument else { return nil }
30+
self = .position(position)
31+
case "associatedValueName":
32+
guard let name = firstStringArgument?.content.text else { return nil }
33+
self = .associatedValueName(name)
34+
case "firstMatchingType":
35+
self = .firstMatchingType
36+
default:
37+
return nil
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import SwiftDiagnostics
2+
import SwiftSyntax
3+
import SwiftSyntaxMacros
4+
5+
enum ExtractCaseValueMacroDiagnostic {
6+
case requiresEnum
7+
case requiresArgs
8+
case requiresPropertyNameArg
9+
case requiresPropertyNameStringLiteral
10+
case requiresGenericType
11+
case noValue(case: String, index: Int)
12+
case noMatchingType(type: String, case: String)
13+
case noAssociatedValues(case: String)
14+
case noAssociatedValueForName(name: String, case: String)
15+
case typeMismatch(case: String, index: Int)
16+
case typeMismatchNamed(name: String, case: String)
17+
}
18+
19+
extension ExtractCaseValueMacroDiagnostic: DiagnosticMessage {
20+
func diagnose(at node: some SyntaxProtocol, fixIts: [FixIt] = []) -> Diagnostic {
21+
Diagnostic(node: Syntax(node), message: self, fixIts: fixIts)
22+
}
23+
24+
var message: String {
25+
switch self {
26+
case .requiresEnum:
27+
return "'ExtractCaseValue' macro can only be applied to an enum"
28+
29+
case .requiresArgs:
30+
return "'ExtractCaseValue' macro requires arguments"
31+
32+
case .requiresPropertyNameArg:
33+
return "'ExtractCaseValue' macro requires `\(caseParamExtractionPropertyNameArgumentLabel)` argument"
34+
35+
case .requiresPropertyNameStringLiteral:
36+
return "'ExtractCaseValue' macro argument `\(caseParamExtractionPropertyNameArgumentLabel)` must be a string literal"
37+
38+
case .requiresGenericType:
39+
return "'ExtractCaseValue' macro requires a generic type for the computed property"
40+
41+
case let .noValue(caseName, index):
42+
return "'ExtractCaseValue' macro could not find an associated value for `\(caseName)` at index \(index). Consider using a default value."
43+
44+
case let .noMatchingType(type, caseName):
45+
return "'ExtractCaseValue' macro found no associated value of type \(type) in `\(caseName)`. Consider using a default value."
46+
47+
case let .noAssociatedValues(caseName):
48+
return "'ExtractCaseValue' macro could not find associated values for `\(caseName)`. Consider using a default value."
49+
50+
case let .noAssociatedValueForName(name, caseName):
51+
return "'ExtractCaseValue' macro found no associated value named \(name) in `\(caseName)`. Consider using a default value."
52+
53+
case let .typeMismatch(caseName, index):
54+
return "'ExtractCaseValue' macro found a mismatching type for `\(caseName)` at index \(index)"
55+
56+
case let .typeMismatchNamed(paramName, caseName):
57+
return "'ExtractCaseValue' macro found a mismatching type for \(paramName) in the `\(caseName)` case"
58+
}
59+
}
60+
61+
var severity: DiagnosticSeverity { .error }
62+
63+
var diagnosticID: MessageID {
64+
MessageID(domain: "Swift", id: "ExtractCaseValue.\(self)")
65+
}
66+
}
67+
68+
struct InsertDefaultValueItMessage: FixItMessage {
69+
var message: String {
70+
"Insert default value"
71+
}
72+
73+
var fixItID: MessageID {
74+
MessageID(domain: "Swift", id: "ExtractCaseValue.\(self)")
75+
}
76+
}

0 commit comments

Comments
 (0)