Skip to content
Merged
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
28 changes: 24 additions & 4 deletions Sources/CasePathsMacrosSupport/CasePathableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import SwiftSyntaxMacros
public struct CasePathableMacro {
static let moduleName = "CasePaths"
static let casePathTypeName = "AnyCasePath"
static let casePathableExpandingMacroNames = [
"CaseBindable",
"Table",
"Selection"
]

private static func shouldGenerate(
for node: AttributeSyntax,
Expand All @@ -15,11 +20,26 @@ public struct CasePathableMacro {
type.as(IdentifierTypeSyntax.self)?.name.text == "CasePathable"
|| type.as(MemberTypeSyntax.self)?.name.text == "CasePathable"
}
if isCasePathable(node.attributeName) { return true }
return !declaration.attributes.contains { element in
guard case let .attribute(attribute) = element else { return false }
return isCasePathable(attribute.attributeName)
guard !isCasePathable(node.attributeName)
else { return true }

let hasCasePathableApplied = declaration.attributes.contains {
$0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "CasePathable"
}
let firstCasePathableExpandableMacroIndex = casePathableExpandingMacroNames.compactMap { macroName in
declaration.attributes.firstIndex {
$0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text ==
macroName
}
}
.sorted()
.first
let nodeIndex = declaration.attributes.firstIndex {
$0.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text ==
node.attributeName.as(IdentifierTypeSyntax.self)?.name.text ?? ""
}

return !hasCasePathableApplied && firstCasePathableExpandableMacroIndex == nodeIndex
}
}

Expand Down
301 changes: 249 additions & 52 deletions Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,11 @@
import SwiftSyntaxMacros
import Testing

private enum CaseBindableMacro {}

extension CaseBindableMacro: ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
try CasePathableMacro.expansion(
of: node,
attachedTo: declaration,
providingExtensionsOf: type,
conformingTo: protocols,
in: context
)
}
}

extension CaseBindableMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
var decls = try CasePathableMacro.expansion(
of: node,
providingMembersOf: declaration,
in: context
)
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return decls }
let elements = enumDecl.memberBlock.members
.flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] }
let cases = elements.map { element -> String in
let hasPayload = element.parameterClause.map { !$0.parameters.isEmpty } ?? false
guard hasPayload else { return "case \(element.name.text)" }
let type = CasePathableMacro.valueType(for: element)
return "case \(element.name.text)(SwiftUI.Binding<\(type)>)"
}
decls.append(
"""
public enum BindingEnumeration {
\(raw: cases.joined(separator: "\n"))
}
"""
)
return decls
}
}

@Suite(
.macros([
CaseBindableMacro.self,
CasePathableMacro.self,
SelectionMacro.self,
])
)
struct CasePathsMacrosSupportTests {
Expand Down Expand Up @@ -225,5 +174,253 @@
"""#
}
}


@Test
func `multiple macros that expand @CasePathable without @CasePathable`() {
assertMacro {
"""
@CaseBindable
@Selection
enum Foo {
case bar
case baz(Int)
case fizz(buzz: String)
}
"""
} expansion: {
#"""
enum Foo {
case bar
case baz(Int)
case fizz(buzz: String)

public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence {
public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath<Foo> {
if root.is(\.bar) {
return \.bar
}
if root.is(\.baz) {
return \.baz
}
if root.is(\.fizz) {
return \.fizz
}
return \.never
}
public var bar: CasePaths.AnyCasePath<Foo, Void> {
._$embed({
Foo.bar
}) {
guard case .bar = $0 else {
return nil
}
return ()
}
}
public var baz: CasePaths.AnyCasePath<Foo, Int> {
._$embed(Foo.baz) {
guard case let .baz(v0) = $0 else {
return nil
}
return v0
}
}
public var fizz: CasePaths.AnyCasePath<Foo, String> {
._$embed(Foo.fizz) {
guard case let .fizz(v0) = $0 else {
return nil
}
return v0
}
}
public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath<Foo>]> {
var allCasePaths: [CasePaths.PartialCaseKeyPath<Foo>] = []
allCasePaths.append(\.bar)
allCasePaths.append(\.baz)
allCasePaths.append(\.fizz)
return allCasePaths.makeIterator()
}
}

public static var allCasePaths: AllCasePaths {
AllCasePaths()
}

public enum BindingEnumeration {
case bar
case baz(SwiftUI.Binding<Int>)
case fizz(SwiftUI.Binding<String>)
}
}

extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable {
}
"""#
}
}

@Test
func `multiple macros that expand @CasePathable with @CasePathable`() {
assertMacro {
"""
@CasePathable
@CaseBindable
@Selection
enum Foo {
case bar
case baz(Int)
case fizz(buzz: String)
}
"""
} expansion: {
#"""
enum Foo {
case bar
case baz(Int)
case fizz(buzz: String)

public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence {
public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath<Foo> {
if root.is(\.bar) {
return \.bar
}
if root.is(\.baz) {
return \.baz
}
if root.is(\.fizz) {
return \.fizz
}
return \.never
}
public var bar: CasePaths.AnyCasePath<Foo, Void> {
._$embed({
Foo.bar
}) {
guard case .bar = $0 else {
return nil
}
return ()
}
}
public var baz: CasePaths.AnyCasePath<Foo, Int> {
._$embed(Foo.baz) {
guard case let .baz(v0) = $0 else {
return nil
}
return v0
}
}
public var fizz: CasePaths.AnyCasePath<Foo, String> {
._$embed(Foo.fizz) {
guard case let .fizz(v0) = $0 else {
return nil
}
return v0
}
}
public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath<Foo>]> {
var allCasePaths: [CasePaths.PartialCaseKeyPath<Foo>] = []
allCasePaths.append(\.bar)
allCasePaths.append(\.baz)
allCasePaths.append(\.fizz)
return allCasePaths.makeIterator()
}
}

public static var allCasePaths: AllCasePaths {
AllCasePaths()
}

public enum BindingEnumeration {
case bar
case baz(SwiftUI.Binding<Int>)
case fizz(SwiftUI.Binding<String>)
}
}

extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable {
}
"""#
}
}
}

private enum SelectionMacro: ExtensionMacro, MemberMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
try CasePathableMacro.expansion(
of: node,
attachedTo: declaration,
providingExtensionsOf: type,
conformingTo: protocols,
in: context
)
}
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
try CasePathableMacro.expansion(
of: node,
providingMembersOf: declaration,
in: context
)
}
}

private enum CaseBindableMacro: MemberMacro, ExtensionMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
try CasePathableMacro.expansion(
of: node,
attachedTo: declaration,
providingExtensionsOf: type,
conformingTo: protocols,
in: context
)
}

static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
var decls = try CasePathableMacro.expansion(
of: node,
providingMembersOf: declaration,
in: context
)
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return decls }
let elements = enumDecl.memberBlock.members
.flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] }
let cases = elements.map { element -> String in
let hasPayload = element.parameterClause.map { !$0.parameters.isEmpty } ?? false
guard hasPayload else { return "case \(element.name.text)" }
let type = CasePathableMacro.valueType(for: element)
return "case \(element.name.text)(SwiftUI.Binding<\(type)>)"
}
decls.append(
"""
public enum BindingEnumeration {
\(raw: cases.joined(separator: "\n"))
}
"""
)
return decls
}
}
#endif
Loading