Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public struct Enum: DeclGroupProtocol, RepresentableBySyntax {
_syntax.name.withoutTrivia().text
}

public var rawRepresentableType: EnumRawRepresentableType? {
EnumRawRepresentableType(possibleRawType: _syntax.inheritanceClause?.inheritedTypes.first)
}

/// Initializes an `Enum` instance with the given syntax node.
///
/// - Parameter syntax: The syntax node representing the `enum` declaration.
Expand All @@ -19,12 +23,17 @@ public struct Enum: DeclGroupProtocol, RepresentableBySyntax {

/// The `enum`'s cases.
public var cases: [EnumCase] {
_syntax.memberBlock.members
var lastSeen: EnumCase?
return _syntax.memberBlock.members
.compactMap { member in
member.decl.as(EnumCaseDeclSyntax.self)
}
.flatMap { syntax in
syntax.elements.map(EnumCase.init)
syntax.elements.map {
let next = EnumCase($0, rawRepresentableType: rawRepresentableType, precedingCase: lastSeen)
lastSeen = next
return next
}
}
}
}
55 changes: 43 additions & 12 deletions Sources/MacroToolkit/EnumCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,58 @@ import SwiftSyntax
public struct EnumCase {
public var _syntax: EnumCaseElementSyntax

public init(_ syntax: EnumCaseElementSyntax) {
public init(_ syntax: EnumCaseElementSyntax, rawRepresentableType: EnumRawRepresentableType? = nil, precedingCase: EnumCase? = nil) {
_syntax = syntax
value = if let rawValue = _syntax.rawValue {
.rawValue(rawValue)
} else if let associatedValue = _syntax.parameterClause {
.associatedValue(Array(associatedValue.parameters).map(EnumCaseAssociatedValueParameter.init))
} else if let rawRepresentableType {
switch rawRepresentableType {
case .string: .inferredRawValue(.init(value: "\"\(raw: _syntax.name.text)\"" as ExprSyntax))
case .character: nil // Characters cannot be inferred
case .integer, .floatingPoint: .inferredRawValue(.init(value: "\(raw: (previousValue() ?? -1) + 1)" as ExprSyntax))
Comment on lines +15 to +17
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the values (after the colons) onto new lines to reduce line length and make things a bit easier to skim

}
} else {
nil
}

/// Raw representable conformance is only synthesized when using integer literals (eg 1), not float literals (eg 1.0).
func previousValue() -> Int? {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this helper function to the top of the function to make things read a bit nicer.

precedingCase?.rawValue.flatMap(IntegerLiteral.init)?.value
}
}

/// The case's name
public var identifier: String {
_syntax.name.withoutTrivia().description
}

/// The value associated with the enum case (either associated or raw).
public var value: EnumCaseValue? {
if let rawValue = _syntax.rawValue {
return .rawValue(rawValue)
} else if let associatedValue = _syntax.parameterClause {
let parameters = Array(associatedValue.parameters)
.map(EnumCaseAssociatedValueParameter.init)
return .associatedValue(parameters)
} else {
return nil

/// The value associated with the enum case (either associated, raw or inferred).
public var value: EnumCaseValue?

/// Helper that gets the associated values from `EnumCase.value` or returns an empty array.
public var associatedValues: [EnumCaseAssociatedValueParameter] {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be renamed to make it clear that it's the types of the associated values and not actual values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To what?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe associatedValueParameters? (given that they include labels as well)

switch value {
case .associatedValue(let values): values
default: []
}
}

/// Helper that gets the raw or inferred raw value from `EnumCase.value` or returns nil.
public var rawValue: ExprSyntax? {
switch value {
case .rawValue(let initializer), .inferredRawValue(let initializer): initializer.value
default: nil
}
}

/// Helper that gets the raw or inferred raw value text, eg "value", 1, or 1.0.
public var rawValueText: String? {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this essentially equivalent to rawValue?.description with a bit of added normalisation? I feel like in situations where you'd use this, you don't need the normalisation anyway, so rawValue?.description would suffice (in which case this property shouldn't be necessary).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no idea that was possible so just my inexperience with swift syntax showing.

rawValue.flatMap(StringLiteral.init)?.value.map { "\"\($0)\"" } ??
rawValue.flatMap(IntegerLiteral.init)?.value.description ??
rawValue.flatMap(FloatLiteral.init)?.value.description
}

public func withoutValue() -> Self {
EnumCase(_syntax.with(\.rawValue, nil).with(\.parameterClause, nil))
Expand Down
1 change: 1 addition & 0 deletions Sources/MacroToolkit/EnumCaseValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import SwiftSyntax
public enum EnumCaseValue {
case associatedValue([EnumCaseAssociatedValueParameter])
case rawValue(InitializerClauseSyntax)
case inferredRawValue(InitializerClauseSyntax)
}
26 changes: 26 additions & 0 deletions Sources/MacroToolkit/EnumRawRepresentableType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftSyntax

/// Enum raw values can be strings, characters, or any of the integer or floating-point number types.
public enum EnumRawRepresentableType {
case string(syntax: IdentifierTypeSyntax)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every case has the same associated value, so I feel it may be better to turn this enum into a struct with a nested Kind enum (containing these cases but without associated values) alongside kind and identifier fields.

case character(syntax: IdentifierTypeSyntax)
case integer(syntax: IdentifierTypeSyntax)
case floatingPoint(syntax: IdentifierTypeSyntax)

init?(possibleRawType syntax: InheritedTypeSyntax?) {
guard let type = syntax?.type.as(IdentifierTypeSyntax.self) else { return nil }
switch type.name.text {
case "String", "NSString":
self = .string(syntax: type)
case "Character":
self = .character(syntax: type)
case "Int", "Int8", "Int16", "Int32", "Int64", "Int128",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128":
self = .integer(syntax: type)
case "Float", "Float16", "Float32", "Float64",
"Double", "CGFloat", "NSNumber":
self = .floatingPoint(syntax: type)
default: return nil
}
}
}
92 changes: 92 additions & 0 deletions Tests/MacroToolkitTests/DeclGroupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,96 @@ final class DeclGroupTests: XCTestCase {
XCTAssertEqual(testClass.accessLevel, .public)
XCTAssertEqual(testClass.declarationContext, nil)
}

func testEnumRawTypeInferredValueString() throws {
let decl: DeclSyntax = """
enum TestEnum: String { case caseOne, caseTwo, caseThree = "case3" }
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 1)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .string = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), [#""caseOne""#, #""caseTwo""#, #""case3""#])
}

func testEnumRawTypeInferredValueInt() throws {
let decl: DeclSyntax = """
enum TestEnum: Int { case caseOne = 1, caseTwo, caseThree }
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 1)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .integer = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), ["1", "2", "3"])
}

func testEnumRawTypeInferredValueNegativeInt() throws {
let decl: DeclSyntax = """
enum TestEnum: Int { case a = -1, b, c }
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 1)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .integer = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), ["-1", "0", "1"])
}

func testEnumRawTypeInferredValueIntMiddle() throws {
let decl: DeclSyntax = """
enum TestEnum: Int { case a, b = 5, c }
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 1)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .integer = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), ["0", "5", "6"])
}

func testEnumRawTypeInferredValueDouble() throws {
let decl: DeclSyntax = """
enum TestEnum: Double {
case caseOne = 1
case caseTwo
case caseThree
}
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 3)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .floatingPoint = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), ["1", "2", "3"])
}

func testEnumRawTypeInferredValueDoubleLiteral() throws {
let decl: DeclSyntax = """
enum TestEnum: Double {
case caseOne = 1.1
case caseTwo = 1.2
case caseThree = 1.3
}
"""
let enumDecl = decl.as(EnumDeclSyntax.self)!
let testEnum = Enum(enumDecl)

XCTAssertEqual(testEnum.identifier, "TestEnum")
XCTAssertEqual(testEnum.members.count, 3)
XCTAssertEqual(testEnum.cases.count, 3)
guard case .floatingPoint = testEnum.rawRepresentableType else { return XCTFail() }
XCTAssertEqual(testEnum.cases.map(\.rawValueText), ["1.1", "1.2", "1.3"])
}
}