Skip to content

Add fixed source to assertMacroExpansion #2021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)
}
}