mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Silence test warnings * wip * wip * wip * update a bunch of docs * wip * wip * fix * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Kill integration tests for now * wip * wip * wip * wip * updating docs for @Reducer macro * replaced more Reducer protocols with @Reducer * Fixed some broken docc references * wip * Some @Reducer docs * more docs * convert some old styles to new style * wip * wip * wip * wip * wip * wip * wip * bump * update tutorials to use body * update tutorials to use DML on destination state enum * Add diagnostic * wip * updated a few more tests * wip * wip * Add another gotcha * wip * wip * wip * fixes * wip * wip * wip * wip * wip * fix * wip * remove for now * wip * wip * updated some docs * migration guides * more migration guide * fix ci * fix * soft deprecate all apis using AnyCasePath * wip * Fix * fix tests * swift-format 509 compatibility * wip * wip * Update Sources/ComposableArchitecture/Macros.swift Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com> * wip * wip * update optional state case study * remove initializer * Don't use @State for BasicsView integration demo * fix tests * remove reduce diagnostics for now * diagnose error not warning * Update Sources/ComposableArchitecture/Macros.swift Co-authored-by: Jesse Tipton <jesse@jessetipton.com> * wip * move integration tests to cron * Revert "move integration tests to cron" This reverts commitf9bdf2f04b. * disable flakey tests on CI * wip * wip * Revert "Revert "move integration tests to cron"" This reverts commit66aafa7327. * fix * wip * fix --------- Co-authored-by: Brandon Williams <mbrandonw@hey.com> Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com> Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Co-authored-by: Jesse Tipton <jesse@jessetipton.com>
174 lines
5.5 KiB
Swift
174 lines
5.5 KiB
Swift
import SwiftDiagnostics
|
|
import SwiftOperators
|
|
import SwiftSyntax
|
|
import SwiftSyntaxBuilder
|
|
import SwiftSyntaxMacroExpansion
|
|
import SwiftSyntaxMacros
|
|
|
|
public enum ReducerMacro {
|
|
}
|
|
|
|
extension ReducerMacro: ExtensionMacro {
|
|
public static func expansion<D: DeclGroupSyntax, T: TypeSyntaxProtocol, C: MacroExpansionContext>(
|
|
of node: AttributeSyntax,
|
|
attachedTo declaration: D,
|
|
providingExtensionsOf type: T,
|
|
conformingTo protocols: [TypeSyntax],
|
|
in context: C
|
|
) throws -> [ExtensionDeclSyntax] {
|
|
if let inheritanceClause = declaration.inheritanceClause,
|
|
inheritanceClause.inheritedTypes.contains(
|
|
where: {
|
|
["Reducer"].withQualified.contains($0.type.trimmedDescription)
|
|
}
|
|
)
|
|
{
|
|
return []
|
|
}
|
|
let ext: DeclSyntax =
|
|
"""
|
|
extension \(type.trimmed): ComposableArchitecture.Reducer {}
|
|
"""
|
|
return [ext.cast(ExtensionDeclSyntax.self)]
|
|
}
|
|
}
|
|
|
|
extension ReducerMacro: MemberAttributeMacro {
|
|
public static func expansion<D: DeclGroupSyntax, M: DeclSyntaxProtocol, C: MacroExpansionContext>(
|
|
of node: AttributeSyntax,
|
|
attachedTo declaration: D,
|
|
providingAttributesFor member: M,
|
|
in context: C
|
|
) throws -> [AttributeSyntax] {
|
|
if let enumDecl = member.as(EnumDeclSyntax.self) {
|
|
var attributes: [String] = []
|
|
switch enumDecl.name.text {
|
|
case "State":
|
|
attributes = ["CasePathable", "dynamicMemberLookup"]
|
|
case "Action":
|
|
attributes = ["CasePathable"]
|
|
default:
|
|
break
|
|
}
|
|
for attribute in enumDecl.attributes {
|
|
guard
|
|
case let .attribute(attribute) = attribute,
|
|
let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text
|
|
else { continue }
|
|
attributes.removeAll(where: { $0 == attributeName })
|
|
}
|
|
return attributes.map {
|
|
AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier($0)))
|
|
}
|
|
} else if let property = member.as(VariableDeclSyntax.self),
|
|
property.bindingSpecifier.text == "var",
|
|
property.bindings.count == 1,
|
|
let binding = property.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
identifier.text == "body",
|
|
case .getter = binding.accessorBlock?.accessors
|
|
{
|
|
if let reduce = declaration.memberBlock.members.first(where: {
|
|
guard
|
|
let method = $0.decl.as(FunctionDeclSyntax.self),
|
|
method.name.text == "reduce",
|
|
method.signature.parameterClause.parameters.count == 2,
|
|
let state = method.signature.parameterClause.parameters.first,
|
|
state.firstName.text == "into",
|
|
state.type.as(AttributedTypeSyntax.self)?.specifier?.text == "inout",
|
|
method.signature.parameterClause.parameters.last?.firstName.text == "action",
|
|
method.signature.effectSpecifiers == nil,
|
|
method.signature.returnClause?.type.as(IdentifierTypeSyntax.self) != nil
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
}) {
|
|
let reduce = reduce.decl.cast(FunctionDeclSyntax.self)
|
|
let visitor = ReduceVisitor(viewMode: .all)
|
|
visitor.walk(declaration)
|
|
context.diagnose(
|
|
Diagnostic(
|
|
node: reduce.name,
|
|
message: MacroExpansionErrorMessage(
|
|
"""
|
|
A 'reduce' method should not be defined in a reducer with a 'body'; it takes \
|
|
precedence and 'body' will never be invoked
|
|
"""
|
|
),
|
|
notes: [
|
|
Note(
|
|
node: Syntax(identifier),
|
|
message: MacroExpansionNoteMessage("'body' defined here")
|
|
)
|
|
]
|
|
)
|
|
)
|
|
}
|
|
for attribute in property.attributes {
|
|
guard
|
|
case let .attribute(attribute) = attribute,
|
|
let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text
|
|
else { continue }
|
|
guard
|
|
!attributeName.starts(with: "ReducerBuilder"),
|
|
!attributeName.starts(with: "ComposableArchitecture.ReducerBuilder")
|
|
else { return [] }
|
|
}
|
|
return [
|
|
AttributeSyntax(
|
|
attributeName: IdentifierTypeSyntax(
|
|
name: .identifier("ComposableArchitecture.ReducerBuilder<Self.State, Self.Action>")
|
|
)
|
|
)
|
|
]
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Array where Element == String {
|
|
var withQualified: Self {
|
|
self.flatMap { [$0, "ComposableArchitecture.\($0)"] }
|
|
}
|
|
}
|
|
|
|
struct MacroExpansionNoteMessage: NoteMessage {
|
|
var message: String
|
|
|
|
init(_ message: String) {
|
|
self.message = message
|
|
}
|
|
|
|
var fixItID: MessageID {
|
|
MessageID(domain: diagnosticDomain, id: "\(Self.self)")
|
|
}
|
|
}
|
|
|
|
private let diagnosticDomain: String = "ComposableArchitectureMacros"
|
|
|
|
private final class ReduceVisitor: SyntaxVisitor {
|
|
var changes: [FixIt.Change] = []
|
|
|
|
override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind {
|
|
guard node.baseName.text == "reduce" else { return super.visit(node) }
|
|
guard
|
|
node.argumentNames == nil
|
|
|| node.argumentNames?.arguments.map(\.name.text) == ["into", "action"]
|
|
else { return super.visit(node) }
|
|
if let base = node.parent?.as(MemberAccessExprSyntax.self)?.base,
|
|
base.as(DeclReferenceExprSyntax.self)?.baseName.tokenKind != .keyword(Keyword.`self`)
|
|
{
|
|
return super.visit(node)
|
|
}
|
|
self.changes.append(
|
|
.replace(
|
|
oldNode: Syntax(node),
|
|
newNode: Syntax(node.with(\.baseName, "update"))
|
|
)
|
|
)
|
|
return .visitChildren
|
|
}
|
|
}
|