Skip to content

Commit 63a2f85

Browse files
committed
Add fixed source to assertMacroExpansion
1 parent b57df1e commit 63a2f85

File tree

4 files changed

+140
-55
lines changed

4 files changed

+140
-55
lines changed

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

+17
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,18 @@ func assertDiagnostic(
254254
/// - macros: The macros that should be expanded, provided as a dictionary
255255
/// mapping macro names (e.g., `"stringify"`) to implementation types
256256
/// (e.g., `StringifyMacro.self`).
257+
/// - 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.
258+
/// - fixedSource: If specified, asserts that the source code after applying Fix-Its matches this string.
257259
/// - testModuleName: The name of the test module to use.
258260
/// - testFileName: The name of the test file name to use.
261+
/// - indentationWidth: The indentation width used in the expansion.
259262
public func assertMacroExpansion(
260263
_ originalSource: String,
261264
expandedSource expectedExpandedSource: String,
262265
diagnostics: [DiagnosticSpec] = [],
263266
macros: [String: Macro.Type],
267+
applyFixIts: [String]? = nil,
268+
fixedSource expectedFixedSource: String? = nil,
264269
testModuleName: String = "TestModule",
265270
testFileName: String = "test.swift",
266271
indentationWidth: Trivia = .spaces(4),
@@ -317,4 +322,16 @@ public func assertMacroExpansion(
317322
assertDiagnostic(actualDiag, in: context, expected: expectedDiag)
318323
}
319324
}
325+
326+
// Applying Fix-Its
327+
if let expectedFixedSource = expectedFixedSource {
328+
let fixedTree = FixItApplier.applyFixes(in: context.diagnostics, withMessages: applyFixIts, to: expandedSourceFile)
329+
let fixedTreeDescription = fixedTree.description
330+
assertStringsEqualWithDiff(
331+
fixedTreeDescription.trimmingTrailingWhitespace(),
332+
expectedFixedSource.trimmingTrailingWhitespace(),
333+
file: file,
334+
line: line
335+
)
336+
}
320337
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
import SwiftDiagnostics
15+
16+
public class FixItApplier: SyntaxRewriter {
17+
var changes: [FixIt.Change]
18+
19+
init(diagnostics: [Diagnostic], withMessages messages: [String]?) {
20+
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
21+
22+
self.changes =
23+
diagnostics
24+
.flatMap { $0.fixIts }
25+
.filter {
26+
return messages.contains($0.message.message)
27+
}
28+
.flatMap { $0.changes }
29+
30+
super.init(viewMode: .all)
31+
}
32+
33+
public override func visitAny(_ node: Syntax) -> Syntax? {
34+
for change in changes {
35+
switch change {
36+
case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id:
37+
return newNode
38+
default:
39+
break
40+
}
41+
}
42+
return nil
43+
}
44+
45+
public override func visit(_ node: TokenSyntax) -> TokenSyntax {
46+
var modifiedNode = node
47+
for change in changes {
48+
switch change {
49+
case .replaceLeadingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
50+
modifiedNode = node.with(\.leadingTrivia, newTrivia)
51+
case .replaceTrailingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
52+
modifiedNode = node.with(\.trailingTrivia, newTrivia)
53+
default:
54+
break
55+
}
56+
}
57+
return modifiedNode
58+
}
59+
60+
/// If `messages` is `nil`, applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree.
61+
/// If `messages` is not `nil`, applies only Fix-Its whose message is in `messages`.
62+
public static func applyFixes<T: SyntaxProtocol>(in diagnostics: [Diagnostic], withMessages messages: [String]?, to tree: T) -> Syntax {
63+
let applier = FixItApplier(diagnostics: diagnostics, withMessages: messages)
64+
return applier.rewrite(tree)
65+
}
66+
}

Tests/SwiftParserTest/Assertions.swift

-52
Original file line numberDiff line numberDiff line change
@@ -276,58 +276,6 @@ struct DiagnosticSpec {
276276
}
277277
}
278278

279-
class FixItApplier: SyntaxRewriter {
280-
var changes: [FixIt.Change]
281-
282-
init(diagnostics: [Diagnostic], withMessages messages: [String]?) {
283-
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
284-
285-
self.changes =
286-
diagnostics
287-
.flatMap { $0.fixIts }
288-
.filter {
289-
return messages.contains($0.message.message)
290-
}
291-
.flatMap { $0.changes }
292-
293-
super.init(viewMode: .all)
294-
}
295-
296-
public override func visitAny(_ node: Syntax) -> Syntax? {
297-
for change in changes {
298-
switch change {
299-
case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id:
300-
return newNode
301-
default:
302-
break
303-
}
304-
}
305-
return nil
306-
}
307-
308-
override func visit(_ node: TokenSyntax) -> TokenSyntax {
309-
var modifiedNode = node
310-
for change in changes {
311-
switch change {
312-
case .replaceLeadingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
313-
modifiedNode = node.with(\.leadingTrivia, newTrivia)
314-
case .replaceTrailingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
315-
modifiedNode = node.with(\.trailingTrivia, newTrivia)
316-
default:
317-
break
318-
}
319-
}
320-
return modifiedNode
321-
}
322-
323-
/// If `messages` is `nil`, applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree.
324-
/// If `messages` is not `nil`, applies only Fix-Its whose message is in `messages`.
325-
public static func applyFixes<T: SyntaxProtocol>(in diagnostics: [Diagnostic], withMessages messages: [String]?, to tree: T) -> Syntax {
326-
let applier = FixItApplier(diagnostics: diagnostics, withMessages: messages)
327-
return applier.rewrite(tree)
328-
}
329-
}
330-
331279
/// Assert that `location` is the same as that of `locationMarker` in `tree`.
332280
func assertLocation<T: SyntaxProtocol>(
333281
_ location: SourceLocation,

Tests/SwiftSyntaxMacroExpansionTest/MacroSystemTests.swift

+57-3
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,37 @@ public struct AddCompletionHandler: PeerMacro {
284284

285285
// This only makes sense for async functions.
286286
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
287-
throw MacroExpansionErrorMessage(
288-
"@addCompletionHandler requires an async function"
287+
let newEffects: FunctionEffectSpecifiersSyntax
288+
if let existingEffects = funcDecl.signature.effectSpecifiers {
289+
newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async))
290+
} else {
291+
newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async))
292+
}
293+
294+
let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects)
295+
296+
let diag = Diagnostic(
297+
node: Syntax(funcDecl.funcKeyword),
298+
message: MacroExpansionErrorMessage(
299+
"can only add a completion-handler variant to an 'async' function"
300+
),
301+
fixIts: [
302+
FixIt(
303+
message: MacroExpansionFixItMessage(
304+
"add 'async'"
305+
),
306+
changes: [
307+
FixIt.Change.replace(
308+
oldNode: Syntax(funcDecl.signature),
309+
newNode: Syntax(newSignature)
310+
)
311+
]
312+
)
313+
]
289314
)
315+
316+
context.diagnose(diag)
317+
return []
290318
}
291319

292320
// Form the completion handler parameter.
@@ -1109,6 +1137,33 @@ final class MacroSystemTests: XCTestCase {
11091137
)
11101138
}
11111139

1140+
func testAddCompletionHandlerWithNoAsync() {
1141+
assertMacroExpansion(
1142+
"""
1143+
@addCompletionHandler
1144+
func f(a: Int, for b: String, _ value: Double) -> String { }
1145+
""",
1146+
expandedSource: """
1147+
func f(a: Int, for b: String, _ value: Double) -> String { }
1148+
""",
1149+
diagnostics: [
1150+
DiagnosticSpec(
1151+
message: "can only add a completion-handler variant to an 'async' function",
1152+
line: 2,
1153+
column: 1,
1154+
fixIts: [
1155+
FixItSpec(message: "add 'async'")
1156+
]
1157+
)
1158+
],
1159+
macros: testMacros,
1160+
fixedSource: """
1161+
func f(a: Int, for b: String, _ value: Double) async -> String { }
1162+
""",
1163+
indentationWidth: indentationWidth
1164+
)
1165+
}
1166+
11121167
func testAddBackingStorage() {
11131168
assertMacroExpansion(
11141169
"""
@@ -1582,5 +1637,4 @@ final class MacroSystemTests: XCTestCase {
15821637
indentationWidth: indentationWidth
15831638
)
15841639
}
1585-
15861640
}

0 commit comments

Comments
 (0)