Skip to content

Commit

Permalink
Add fixed source to assertMacroExpansion
Browse files Browse the repository at this point in the history
  • Loading branch information
kimdv committed Oct 26, 2023
1 parent 598e191 commit f966b2c
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 66 deletions.
7 changes: 7 additions & 0 deletions Release Notes/511.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## New APIs

- `assertMacroExpansion` now have new parameters named `applyFixIts` and `fixedSource`
- Description: `applyFixIts` and `fixedSource` are used to assert so ensure that the source code after applying Fix-Its matches this string.
- Issue: https://github.com/apple/swift-syntax/issues/2015
- Pull Request: https://github.com/apple/swift-syntax/pull/2021

## API Behavior Changes

## Deprecations
Expand All @@ -21,9 +26,11 @@
- Effect specifiers:
- Description: The `unexpectedAfterThrowsSpecifier` node of the various effect specifiers has been removed.
- Pull request: https://github.com/apple/swift-syntax/pull/2219

- `SyntaxKind` removed conformance to `CaseIterable`
- Description: `SyntaxKind` no longer conforms to `CaseIterable` since there is no good use case to iterate over all syntax kinds.
- Pull request: https://github.com/apple/swift-syntax/pull/2292

- `IntegerLiteralExprSyntax.Radix` removed conformance to `CaseIterable`
- Description: `IntegerLiteralExprSyntax.Radix` no longer conforms to `CaseIterable` since there is no good use case to iterate over all radix kinds.
- Pull request: https://github.com/apple/swift-syntax/pull/2292
Expand Down
17 changes: 17 additions & 0 deletions Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,18 @@ func assertDiagnostic(
/// - macros: The macros that should be expanded, provided as a dictionary
/// mapping macro names (e.g., `"stringify"`) to implementation types
/// (e.g., `StringifyMacro.self`).
/// - applyFixIts: If specified, filters the Fix-Its that are applied to generate `fixedSource` to only those whose message occurs in this array. If `nil`, all Fix-Its from the diagnostics are applied.
/// - fixedSource: If specified, asserts that the source code after applying Fix-Its matches this string.
/// - testModuleName: The name of the test module to use.
/// - testFileName: The name of the test file name to use.
/// - indentationWidth: The indentation width used in the expansion.
public func assertMacroExpansion(
_ originalSource: String,
expandedSource expectedExpandedSource: String,
diagnostics: [DiagnosticSpec] = [],
macros: [String: Macro.Type],
applyFixIts: [String]? = nil,
fixedSource expectedFixedSource: String? = nil,
testModuleName: String = "TestModule",
testFileName: String = "test.swift",
indentationWidth: Trivia = .spaces(4),
Expand Down Expand Up @@ -317,4 +322,16 @@ public func assertMacroExpansion(
assertDiagnostic(actualDiag, in: context, expected: expectedDiag)
}
}

// Applying Fix-Its
if let expectedFixedSource = expectedFixedSource {
let fixedTree = FixItApplier.applyFixes(from: context.diagnostics, filterByMessages: applyFixIts, to: origSourceFile)
let fixedTreeDescription = fixedTree.description
assertStringsEqualWithDiff(
fixedTreeDescription.trimmingTrailingWhitespace(),
expectedFixedSource.trimmingTrailingWhitespace(),
file: file,
line: line
)
}
}
186 changes: 120 additions & 66 deletions Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,89 +28,117 @@ import XCTest
final class PeerMacroTests: XCTestCase {
private let indentationWidth: Trivia = .spaces(2)

func testAddCompletionHandler() {
struct AddCompletionHandler: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Only on functions at the moment. We could handle initializers as well
// with a bit of work.
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions")
}
fileprivate struct AddCompletionHandler: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Only on functions at the moment. We could handle initializers as well
// with a bit of work.
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions")
}

// This only makes sense for async functions.
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
throw MacroExpansionErrorMessage(
"@addCompletionHandler requires an async function"
)
// This only makes sense for async functions.
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
let newEffects: FunctionEffectSpecifiersSyntax
if let existingEffects = funcDecl.signature.effectSpecifiers {
newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async))
} else {
newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async))
}

// Form the completion handler parameter.
let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed

let completionHandlerParam =
FunctionParameterSyntax(
firstName: .identifier("completionHandler"),
colon: .colonToken(trailingTrivia: .space),
type: TypeSyntax("(\(resultType ?? "")) -> Void")
)

// Add the completion handler parameter to the parameter list.
let parameterList = funcDecl.signature.parameterClause.parameters
var newParameterList = parameterList
if !parameterList.isEmpty {
// We need to add a trailing comma to the preceding list.
newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space)
}
newParameterList.append(completionHandlerParam)
let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects)

let diag = Diagnostic(
node: Syntax(funcDecl.funcKeyword),
message: MacroExpansionErrorMessage(
"can only add a completion-handler variant to an 'async' function"
),
fixIts: [
FixIt(
message: MacroExpansionFixItMessage(
"add 'async'"
),
changes: [
FixIt.Change.replace(
oldNode: Syntax(funcDecl.signature),
newNode: Syntax(newSignature)
)
]
)
]
)

context.diagnose(diag)
return []
}

let callArguments: [String] = parameterList.map { param in
let argName = param.secondName ?? param.firstName
// Form the completion handler parameter.
let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed

let completionHandlerParam =
FunctionParameterSyntax(
firstName: .identifier("completionHandler"),
colon: .colonToken(trailingTrivia: .space),
type: TypeSyntax("(\(resultType ?? "")) -> Void")
)

// Add the completion handler parameter to the parameter list.
let parameterList = funcDecl.signature.parameterClause.parameters
var newParameterList = parameterList
if !parameterList.isEmpty {
// We need to add a trailing comma to the preceding list.
newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space)
}
newParameterList.append(completionHandlerParam)

if param.firstName.text != "_" {
return "\(param.firstName.text): \(argName.text)"
}
let callArguments: [String] = parameterList.map { param in
let argName = param.secondName ?? param.firstName

return "\(argName.text)"
if param.firstName.text != "_" {
return "\(param.firstName.text): \(argName.text)"
}

let call: ExprSyntax =
"\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))"

// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
// so that the full body could go here.
let newBody: ExprSyntax =
"""
return "\(argName.text)"
}

Task {
completionHandler(await \(call))
}
let call: ExprSyntax =
"\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))"

"""
// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
// so that the full body could go here.
let newBody: ExprSyntax =
"""
// Drop the @addCompletionHandler attribute from the new declaration.
let newAttributeList = funcDecl.attributes.filter {
guard case let .attribute(attribute) = $0 else {
return true
Task {
completionHandler(await \(call))
}
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler"
}
var newFunc = funcDecl
newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async
newFunc.signature.returnClause = nil // drop result type
newFunc.signature.parameterClause.parameters = newParameterList
newFunc.signature.parameterClause.trailingTrivia = []
newFunc.body = CodeBlockSyntax { newBody }
newFunc.attributes = newAttributeList
"""

return [DeclSyntax(newFunc)]
// Drop the @addCompletionHandler attribute from the new declaration.
let newAttributeList = funcDecl.attributes.filter {
guard case let .attribute(attribute) = $0 else {
return true
}
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler"
}

var newFunc = funcDecl
newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async
newFunc.signature.returnClause = nil // drop result type
newFunc.signature.parameterClause.parameters = newParameterList
newFunc.signature.parameterClause.trailingTrivia = []
newFunc.body = CodeBlockSyntax { newBody }
newFunc.attributes = newAttributeList

return [DeclSyntax(newFunc)]
}
}

func testAddCompletionHandler() {
assertMacroExpansion(
"""
@addCompletionHandler
Expand Down Expand Up @@ -193,4 +221,30 @@ final class PeerMacroTests: XCTestCase {
]
)
}

func testAddCompletionHandlerWhereThereIsNotAsync() {
assertMacroExpansion(
"""
@addCompletionHandler
func f(a: Int, for b: String, _ value: Double) -> String { }
""",
expandedSource: """
func f(a: Int, for b: String, _ value: Double) -> String { }
""",
diagnostics: [
DiagnosticSpec(
message: "can only add a completion-handler variant to an 'async' function",
line: 2,
column: 1,
fixIts: [FixItSpec(message: "add 'async'")]
)
],
macros: ["addCompletionHandler": AddCompletionHandler.self],
fixedSource: """
@addCompletionHandler
func f(a: Int, for b: String, _ value: Double) async-> String { }
""",
indentationWidth: indentationWidth
)
}
}

0 comments on commit f966b2c

Please sign in to comment.