-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial implementation of @ProvidesFixture macro (#1)
- Loading branch information
1 parent
2e12c54
commit 7b75bd1
Showing
10 changed files
with
493 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"pins" : [ | ||
{ | ||
"identity" : "swift-syntax", | ||
"kind" : "remoteSourceControl", | ||
"location" : "https://github.com/apple/swift-syntax.git", | ||
"state" : { | ||
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5", | ||
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a" | ||
} | ||
} | ||
], | ||
"version" : 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// swift-tools-version: 5.9 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
import CompilerPluginSupport | ||
|
||
let package = Package( | ||
name: "SwiftFixture", | ||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], | ||
products: [ | ||
.library(name: "SwiftFixture", targets: ["SwiftFixture"]) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), | ||
], | ||
targets: [ | ||
.target( | ||
name: "SwiftFixture", | ||
dependencies: [ | ||
.target(name: "SwiftFixtureMacros") | ||
] | ||
), | ||
.testTarget( | ||
name: "SwiftFixtureTests", | ||
dependencies: [ | ||
.target(name: "SwiftFixture") | ||
] | ||
), | ||
|
||
.macro( | ||
name: "SwiftFixtureMacros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax") | ||
] | ||
), | ||
.testTarget( | ||
name: "SwiftFixtureMacrosTests", | ||
dependencies: [ | ||
.target(name: "SwiftFixtureMacros"), | ||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), | ||
] | ||
) | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#if compiler(>=5.9) | ||
/// A macro used to automatically synthesise ``FixtureProviding`` conformance for a given type. | ||
@attached(member, names: named(provideFixture)) | ||
@attached(conformance) | ||
public macro ProvideFixture() = #externalMacro( | ||
module: "SwiftFixtureMacros", | ||
type: "ProvideFixtureMacro" | ||
) | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Foundation | ||
|
||
enum ExpansionError: Error { | ||
case noInitializers | ||
case tooManyInitializers | ||
} | ||
|
||
extension ExpansionError: CustomDebugStringConvertible { | ||
var debugDescription: String { | ||
switch self { | ||
case .noInitializers: | ||
return """ | ||
@ProvideFixture requires that at least one initializer is defined | ||
""" | ||
case .tooManyInitializers: | ||
return """ | ||
@ProvideFixture is unable to disambiguate between multiple initializers | ||
""" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntaxMacros | ||
|
||
@main | ||
struct Plugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
ProvideFixtureMacro.self | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
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 isThrowing: Bool | ||
} | ||
|
||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingMembersOf declaration: some DeclGroupSyntax, | ||
in context: some MacroExpansionContext | ||
) throws -> [DeclSyntax] { | ||
// Discover the initializer aguments | ||
let initializer = try initializerContext(for: declaration) | ||
|
||
// Create the provideFixutre implementation calling through to the initialiser | ||
// public static func provideFixture(using fixture: Fixture) throws -> Self { ... } | ||
let functionDecl = try FunctionDeclSyntax( | ||
"public static func provideFixture(using fixture: Fixture) throws -> \(initializer.typeIdentifier)" | ||
) { | ||
CodeBlockItemListSyntax { | ||
|
||
// Self(foo: try fixture(), bar: try fixture()) | ||
FunctionCallExprSyntax(callee: IdentifierExprSyntax(identifier: initializer.typeIdentifier)) { | ||
for label in initializer.argumentLabels { | ||
|
||
// foo: try fixture() | ||
TupleExprElementSyntax( | ||
label: label, | ||
expression: TryExprSyntax( | ||
expression: FunctionCallExprSyntax( | ||
callee: IdentifierExprSyntax( | ||
identifier: "fixture" | ||
) | ||
) | ||
) | ||
) | ||
} | ||
} | ||
.wrapInTry(initializer.isThrowing) // try Self(...) | ||
} | ||
} | ||
|
||
return [ | ||
DeclSyntax(functionDecl) | ||
] | ||
} | ||
|
||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingConformancesOf declaration: some DeclGroupSyntax, | ||
in context: some MacroExpansionContext | ||
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { | ||
return [ | ||
("FixtureProviding", nil) | ||
] | ||
} | ||
} | ||
|
||
// MARK: - Arguments | ||
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: TokenSyntax = "Self" // TODO: Try and figure out the actual type later? | ||
|
||
// 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, | ||
argumentLabels: memberwiseInitializerArgumentLabels(for: declaration), | ||
isThrowing: false | ||
) | ||
} | ||
|
||
// Otherwise build the context from the most appropriate initializer decl | ||
return InitializerContext( | ||
decl: try bestInitializer(from: initializers), | ||
typeIdentifier: typeIdentifier | ||
) | ||
} | ||
|
||
private static func bestInitializer( | ||
from initializers: [InitializerDeclSyntax] | ||
) throws -> InitializerDeclSyntax { | ||
if initializers.isEmpty { | ||
throw ExpansionError.noInitializers | ||
} 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 ExpansionError.tooManyInitializers | ||
} | ||
|
||
private static func memberwiseInitializerArgumentLabels( | ||
for declaration: StructDeclSyntax | ||
) -> [String] { | ||
var labels: [String] = [] | ||
|
||
for member in declaration.memberBlock.members { | ||
guard let variable = member.decl.as(VariableDeclSyntax.self) else { continue } | ||
|
||
// for let keywords without initializer values | ||
if variable.bindingKeyword.tokenKind == .keyword(.let) { | ||
for binding in variable.bindings where binding.initializer == nil { | ||
guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else { continue } | ||
labels.append(identifier.identifier.text) | ||
} | ||
} | ||
|
||
// for non-computed vars | ||
if variable.bindingKeyword.tokenKind == .keyword(.var) { | ||
for binding in variable.bindings where binding.accessor == nil { | ||
guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else { continue } | ||
labels.append(identifier.identifier.text) | ||
} | ||
} | ||
} | ||
|
||
return labels | ||
} | ||
} | ||
|
||
// MARK: - Utils | ||
private extension FunctionCallExprSyntax { | ||
func wrapInTry(_ wrapInTry: Bool = true) -> ExprSyntaxProtocol { | ||
if wrapInTry { | ||
return TryExprSyntax(expression: self) | ||
} else { | ||
return self | ||
} | ||
} | ||
} | ||
|
||
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 | ||
switch parameter.firstName.tokenKind { | ||
case .identifier(let label): | ||
return label | ||
case .wildcard: | ||
return nil | ||
default: | ||
// afaik, the external parameter label can only either be a wildcard or an identifier | ||
// TODO: Confirm if this code path is possible | ||
fatalError("Unexpected TokenKind \(parameter.firstName.tokenKind)") | ||
} | ||
} | ||
|
||
self.init( | ||
typeIdentifier: typeIdentifier, | ||
argumentLabels: argumentLabels, | ||
isThrowing: isThrowing | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.