diff --git a/Sources/MacroToolkit/DeclGroup/Enum.swift b/Sources/MacroToolkit/DeclGroup/Enum.swift index c1c7e3f..5c184c0 100644 --- a/Sources/MacroToolkit/DeclGroup/Enum.swift +++ b/Sources/MacroToolkit/DeclGroup/Enum.swift @@ -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. @@ -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 + } } } } diff --git a/Sources/MacroToolkit/EnumCase.swift b/Sources/MacroToolkit/EnumCase.swift index ff68dc7..825ee89 100644 --- a/Sources/MacroToolkit/EnumCase.swift +++ b/Sources/MacroToolkit/EnumCase.swift @@ -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)) + } + } else { + nil + } + + /// Raw representable conformance is only synthesized when using integer literals (eg 1), not float literals (eg 1.0). + func previousValue() -> Int? { + 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] { + 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? { + 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)) diff --git a/Sources/MacroToolkit/EnumCaseValue.swift b/Sources/MacroToolkit/EnumCaseValue.swift index f213877..a362a83 100644 --- a/Sources/MacroToolkit/EnumCaseValue.swift +++ b/Sources/MacroToolkit/EnumCaseValue.swift @@ -4,4 +4,5 @@ import SwiftSyntax public enum EnumCaseValue { case associatedValue([EnumCaseAssociatedValueParameter]) case rawValue(InitializerClauseSyntax) + case inferredRawValue(InitializerClauseSyntax) } diff --git a/Sources/MacroToolkit/EnumRawRepresentableType.swift b/Sources/MacroToolkit/EnumRawRepresentableType.swift new file mode 100644 index 0000000..6b9d8e1 --- /dev/null +++ b/Sources/MacroToolkit/EnumRawRepresentableType.swift @@ -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) + 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 + } + } +} diff --git a/Tests/MacroToolkitTests/DeclGroupTests.swift b/Tests/MacroToolkitTests/DeclGroupTests.swift index 6d22fa2..30f5b42 100644 --- a/Tests/MacroToolkitTests/DeclGroupTests.swift +++ b/Tests/MacroToolkitTests/DeclGroupTests.swift @@ -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"]) + } }