Skip to content

Commit

Permalink
Refactoring (#17)
Browse files Browse the repository at this point in the history
* Fix typo in @ProvideFixture documentation

* Remove redundant argument from initializerReference

* Refactor ProvideFixture macro

* Merge diagnostics into single type

* Simplify FunctionDeclSyntax building

* Move testMacro array into own file

* Extract MacroExpansionExprSyntax initFixture into helper

* Extract wrapInTry into own file
  • Loading branch information
liamnichols authored Jul 18, 2023
1 parent 13daa3a commit 596426f
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 145 deletions.
2 changes: 1 addition & 1 deletion Sources/SwiftFixture/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/// ```swift
/// import SwiftFixture
///
/// @FixtureProviding
/// @ProvideFixture
/// struct User {
/// var id: UUID
/// var name: String
Expand Down
13 changes: 6 additions & 7 deletions Sources/SwiftFixtureMacros/Macros/InitFixtureMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct InitFixtureMacro: ExpressionMacro {

// Read information from each argument
let valueProvider = node.argumentList.first!.expression
let (type, name, arguments) = try initializerReference(from: node.argumentList.last!, context: context)
let (type, name, arguments) = try initializerReference(from: node.argumentList.last!)

// Resolve the callee expression based on the method reference to be used
let identifier = IdentifierExprSyntax(identifier: type)
Expand Down Expand Up @@ -49,9 +49,8 @@ public struct InitFixtureMacro: ExpressionMacro {
})
}

private static func initializerReference<Context: MacroExpansionContext>(
from argument: TupleExprElementListSyntax.Element,
context: Context
private static func initializerReference(
from argument: TupleExprElementListSyntax.Element
) throws -> (type: TokenSyntax, name: TokenSyntax?, arguments: [String?]) {
guard argument.label?.text == "using" else {
fatalError("compiler bug: third macro argument must be the intiailizer function signature")
Expand All @@ -60,13 +59,13 @@ public struct InitFixtureMacro: ExpressionMacro {
guard let expression = argument.expression.as(MemberAccessExprSyntax.self) else {
// TODO: We could offer a fixit suggestion here if the type was defined as a generic argument
throw DiagnosticsError(diagnostics: [
InitFixtureDiagnostic.requiresUnappliedMethodReference.diagnose(at: argument.expression)
DiagnosticMessages.requiresUnappliedMethodReference.diagnose(at: argument.expression)
])
}

guard let base = expression.base?.as(IdentifierExprSyntax.self) else {
throw DiagnosticsError(diagnostics: [
InitFixtureDiagnostic.requiresBaseTypeOfUnappliedMethodReference.diagnose(at: expression.dot)
DiagnosticMessages.requiresBaseTypeOfUnappliedMethodReference.diagnose(at: expression.dot)
])
}

Expand All @@ -82,7 +81,7 @@ public struct InitFixtureMacro: ExpressionMacro {

guard let declNameArguments = expression.declNameArguments else {
throw DiagnosticsError(diagnostics: [
InitFixtureDiagnostic.requiresUnappliedMethodReferenceDeclarationNameArgumentList.diagnose(
DiagnosticMessages.requiresUnappliedMethodReferenceDeclarationNameArgumentList.diagnose(
at: expression,
position: expression.endPosition
)
Expand Down
118 changes: 51 additions & 67 deletions Sources/SwiftFixtureMacros/Macros/ProvideFixtureMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,30 @@ public struct ProvideFixtureMacro: MemberMacro, ConformanceMacro {
/// A type that describes the initializer to be called in the `FixtureProviding.provideFixture(using:)` implementation
struct InitializerContext {
let typeIdentifier: TokenSyntax
let argumentLabels: [String?]
let unappliedMethodReference: MemberAccessExprSyntax
let isThrowing: Bool

var unappliedMethodReference: MemberAccessExprSyntax {
MemberAccessExprSyntax(
base: IdentifierExprSyntax(identifier: typeIdentifier),
name: .keyword(.`init`),
declNameArguments: DeclNameArgumentsSyntax(
arguments: DeclNameArgumentListSyntax(argumentLabels.map { label in
if let label {
DeclNameArgumentSyntax(name: .identifier(label))
} else {
DeclNameArgumentSyntax(name: .wildcardToken())
}
})
)
)
}
}

public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Discover the initializer arguments
let initializer = try initializerContext(for: declaration)
// Discover the context to be used for this declaration
let context = try initializerContext(for: declaration)

// Create the provideFixture implementation calling through to the initialiser
// public static func provideFixture(using values: ValueProvider) throws -> Self { ... }
let valuesId = IdentifierExprSyntax(identifier: "values")
let functionDecl = try FunctionDeclSyntax(
"public static func provideFixture(using values: ValueProvider) throws -> \(initializer.typeIdentifier)"
"public static func provideFixture(using \(valuesId): ValueProvider) throws -> \(context.typeIdentifier)"
) {
CodeBlockItemListSyntax {
// #initFixture(with: values, using: TheType.init(foo:bar:))
MacroExpansionExprSyntax(macro: "initFixture", leftParen: .leftParenToken(), rightParen: .rightParenToken()) {
TupleExprElementSyntax(
label: "with",
expression: IdentifierExprSyntax(identifier: "values")
)

TupleExprElementSyntax(
label: "using",
expression: initializer.unappliedMethodReference
)
}
.wrapInTry(initializer.isThrowing) // try #initFixture(...)
}
// #initFixture(with: values, using: TheType.init(foo:bar:))
MacroExpansionExprSyntax.initFixture(
valueProvider: valuesId,
unappliedMethodReference: context.unappliedMethodReference
)
.wrapInTry(context.isThrowing) // try #initFixture(...)
}

return [
Expand All @@ -77,42 +53,34 @@ public struct ProvideFixtureMacro: MemberMacro, ConformanceMacro {
private extension ProvideFixtureMacro {
static func initializerContext(for declaration: some DeclGroupSyntax) throws -> InitializerContext {
// Find all initializers in the declaration
let initializers = declaration.memberBlock.members.compactMap { $0.decl.as(InitializerDeclSyntax.self) }
let typeIdentifier = try typeIdentifier(from: declaration)
let initializers = declaration.memberBlock.members
.compactMap { $0.decl.as(InitializerDeclSyntax.self) }
.map { InitializerContext(decl: $0, typeIdentifier: typeIdentifier) }

// If there are none, and it's a struct, assume use of the memberwise init
if initializers.isEmpty, let declaration = declaration.as(StructDeclSyntax.self) {
return InitializerContext(
typeIdentifier: typeIdentifier,
name: .keyword(.`init`),
argumentLabels: memberwiseInitializerArgumentLabels(for: declaration),
isThrowing: false
)
}

// Otherwise build the context from the most appropriate initializer decl
return InitializerContext(
decl: try bestInitializer(from: initializers, in: declaration),
typeIdentifier: typeIdentifier
)
}

private static func bestInitializer(
from initializers: [InitializerDeclSyntax],
in declaration: some DeclGroupSyntax
) throws -> InitializerDeclSyntax {
if initializers.isEmpty {
// Otherwise either return the initializer or throw the appropriate error
switch initializers.count {
case 1:
return initializers.first!
case 0:
throw DiagnosticsError(diagnostics: [
DiagnosticMessages.noInitializers.diagnose(at: declaration)
])
default:
throw DiagnosticsError(diagnostics: [
ProvideFixtureDiagnostic.noInitializers.diagnose(at: declaration)
DiagnosticMessages.tooManyInitializers.diagnose(at: declaration)
])
} else if let initializer = initializers.first, initializers.count == 1 {
return initializer
}

// If there are multiple options, either find the first initializer
// TODO: Check for the marker as a reference to disambiguate
throw DiagnosticsError(diagnostics: [
ProvideFixtureDiagnostic.tooManyInitializers.diagnose(at: declaration)
])
}

private static func memberwiseInitializerArgumentLabels(
Expand Down Expand Up @@ -153,7 +121,7 @@ private extension ProvideFixtureMacro {
declaration.identifier
} else {
throw DiagnosticsError(diagnostics: [
ProvideFixtureDiagnostic.unsupportedMember.diagnose(at: declaration)
DiagnosticMessages.unsupportedMember.diagnose(at: declaration)
])
}

Expand All @@ -163,17 +131,32 @@ private extension ProvideFixtureMacro {
}

// MARK: - Utils
private extension ExprSyntaxProtocol {
func wrapInTry(_ wrapInTry: Bool = true) -> ExprSyntaxProtocol {
if wrapInTry {
return TryExprSyntax(expression: self)
} else {
return self
}
private extension ProvideFixtureMacro.InitializerContext {
init(
typeIdentifier: TokenSyntax,
name: TokenSyntax,
argumentLabels: [String?],
isThrowing: Bool
) {
self.init(
typeIdentifier: typeIdentifier,
unappliedMethodReference: MemberAccessExprSyntax(
base: IdentifierExprSyntax(identifier: typeIdentifier),
name: .keyword(.`init`),
declNameArguments: DeclNameArgumentsSyntax(
arguments: DeclNameArgumentListSyntax(argumentLabels.map { label in
if let label {
DeclNameArgumentSyntax(name: .identifier(label))
} else {
DeclNameArgumentSyntax(name: .wildcardToken())
}
})
)
),
isThrowing: isThrowing
)
}
}

private extension ProvideFixtureMacro.InitializerContext {
init(decl: InitializerDeclSyntax, typeIdentifier: TokenSyntax) {
let isThrowing = decl.signature.effectSpecifiers?.throwsSpecifier != nil
let argumentLabels: [String?] = decl.signature.input.parameterList.map { parameter in
Expand All @@ -191,6 +174,7 @@ private extension ProvideFixtureMacro.InitializerContext {

self.init(
typeIdentifier: typeIdentifier,
name: .keyword(.`init`),
argumentLabels: argumentLabels,
isThrowing: isThrowing
)
Expand Down
61 changes: 61 additions & 0 deletions Sources/SwiftFixtureMacros/Utilities/DiagnosticMessages.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import SwiftSyntax
import SwiftDiagnostics

struct DiagnosticMessages: DiagnosticMessage {
let message: String
let diagnosticID: MessageID
let severity: DiagnosticSeverity

private init(
message: String,
id: String,
severity: DiagnosticSeverity = .error
) {
self.message = message
self.diagnosticID = .init(domain: "SwiftFixture", id: id)
self.severity = severity
}

static var requiresUnappliedMethodReference: Self {
Self(
message: "Argument must be an unapplied method reference for a static method or initializer",
id: #function
)
}

static var requiresBaseTypeOfUnappliedMethodReference: Self {
Self(
message: "Unapplied method reference must explicitly define the base type",
id: #function
)

}

static var requiresUnappliedMethodReferenceDeclarationNameArgumentList: Self {
Self(
message: "Declaration name argument list must be provided",
id: #function
)
}

static var unsupportedMember: Self {
Self(
message: "@ProvideFixture cannot be attached to this member",
id: #function
)
}

static var noInitializers: Self {
Self(
message: "@ProvideFixture requires that at least one initializer is defined",
id: #function
)
}

static var tooManyInitializers: Self {
Self(
message: "@ProvideFixture is unable to disambiguate between multiple initializers",
id: #function
)
}
}
11 changes: 11 additions & 0 deletions Sources/SwiftFixtureMacros/Utilities/ExprSyntaxProtocol+Try.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftSyntax

extension ExprSyntaxProtocol {
func wrapInTry(_ wrapInTry: Bool = true) -> ExprSyntaxProtocol {
if wrapInTry {
return TryExprSyntax(expression: self)
} else {
return self
}
}
}
32 changes: 0 additions & 32 deletions Sources/SwiftFixtureMacros/Utilities/InitFixtureDiagnostic.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftSyntax

extension MacroExpansionExprSyntax {
static func initFixture(
valueProvider: some ExprSyntaxProtocol,
unappliedMethodReference: some ExprSyntaxProtocol
) -> Self {
MacroExpansionExprSyntax(macro: "initFixture", leftParen: .leftParenToken(), rightParen: .rightParenToken()) {
TupleExprElementSyntax(
label: "with",
expression: valueProvider
)

TupleExprElementSyntax(
label: "using",
expression: unappliedMethodReference
)
}
}
}

This file was deleted.

Loading

0 comments on commit 596426f

Please sign in to comment.