Sources/MockoloTestSupportMacros/Fixture.swift (114 lines of code) (raw):

import SwiftBasicFormat import SwiftSyntax import SwiftSyntaxMacros struct Fixture: MemberMacro { struct Arguments { var imports: [String]? var testableImports: [String]? var includesConcurrencyHelpers: Bool = false } static func extractArguments(from attribute: AttributeSyntax) throws -> Arguments { var result = Arguments() guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else { return result } for argument in arguments { if argument.label?.text == "includesConcurrencyHelpers", let literal = argument.expression.as(BooleanLiteralExprSyntax.self) { result.includesConcurrencyHelpers = try extract(booleanLiteral: literal) } if argument.label?.text == "imports", let expr = argument.expression.as(ArrayExprSyntax.self) { result.imports = try extract(stringLiteralArray: expr) } if argument.label?.text == "testableImports", let expr = argument.expression.as(ArrayExprSyntax.self) { result.testableImports = try extract(stringLiteralArray: expr) } } if result.includesConcurrencyHelpers && !(result.imports ?? []).contains("Foundation") { result.imports = CollectionOfOne("Foundation") + (result.imports ?? []) } return result func extract(stringLiteralArray: ArrayExprSyntax) throws -> [String] { return try stringLiteralArray.elements.map { element in guard let literal = element.expression.as(StringLiteralExprSyntax.self) else { throw MessageError("Must be string literal.") } guard literal.segments.count == 1 else { throw MessageError("Cannot use string interpolation.") } return literal.segments.description } } func extract(booleanLiteral: BooleanLiteralExprSyntax) throws -> Bool { return switch booleanLiteral.literal.text { case "true": true case "false": false default: throw MessageError("Unexpected literal.") } } } static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { let argument = try extractArguments(from: node) let baseItems = declaration.memberBlock.members.filter { (item: MemberBlockItemSyntax) in if let decl = item.decl.asProtocol(WithAttributesSyntax.self) { let isFixtureAnnotated = decl.attributes.contains { (attr: AttributeListSyntax.Element) in return attr.trimmedDescription == "@Fixture" } return !isFixtureAnnotated } return true } let baseIndent = Trivia(pieces: node.leadingTrivia.filter(\.isSpaceOrTab)) let indent = BasicFormat.inferIndentation(of: declaration) ?? .spaces(4) var sourceContent = baseItems.trimmedDescription(matching: \.isNewline) if let testableImports = argument.testableImports { sourceContent = testableImports.map { "\(baseIndent)\(indent)@testable import \($0)\n" }.joined() + "\n" + sourceContent } if let imports = argument.imports { sourceContent = imports.map { "\(baseIndent)\(indent)import \($0)\n" }.joined() + "\n" + sourceContent } var _sourceInitExpr: ExprSyntax = ExprSyntax(StringLiteralExprSyntax( multilineContent: sourceContent, endIndent: baseIndent + indent )) if argument.includesConcurrencyHelpers { _sourceInitExpr = ExprSyntax( InfixOperatorExprSyntax( leftOperand: _sourceInitExpr, operator: BinaryOperatorExprSyntax(operator: .binaryOperator("+")), rightOperand: #""\n\n" + concurrencyHelpers._generatedSource"# as ExprSyntax ) ) } return [DeclSyntax(VariableDeclSyntax( modifiers: [.init(name: .keyword(.static))], .let, name: "_source", initializer: InitializerClauseSyntax( value: _sourceInitExpr ) ))] } } extension StringLiteralExprSyntax { fileprivate init(multilineContent: String, endIndent: Trivia) { self = StringLiteralExprSyntax( openingPounds: .rawStringPoundDelimiter("##"), openingQuote: .multilineStringQuoteToken(), segments: [.stringSegment(.init(content: .stringSegment(multilineContent)))], closingQuote: .multilineStringQuoteToken(leadingTrivia: endIndent), closingPounds: .rawStringPoundDelimiter("##") ) } }