Skip to content

Commit 8e7a59e

Browse files
committed
Add fixed source to assertMacroExpansion
1 parent 35edf53 commit 8e7a59e

File tree

4 files changed

+144
-55
lines changed

4 files changed

+144
-55
lines changed

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

Lines changed: 17 additions & 0 deletions
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),
@@ -316,4 +321,16 @@ public func assertMacroExpansion(
316321
assertDiagnostic(actualDiag, in: context, expected: expectedDiag)
317322
}
318323
}
324+
325+
// Applying Fix-Its
326+
if let expectedFixedSource = expectedFixedSource {
327+
let fixedTree = FixItApplier.applyFixes(in: context.diagnostics, withMessages: applyFixIts, to: expandedSourceFile)
328+
let fixedTreeDescription = fixedTree.description
329+
assertStringsEqualWithDiff(
330+
fixedTreeDescription.trimmingTrailingWhitespace(),
331+
expectedFixedSource.trimmingTrailingWhitespace(),
332+
file: file,
333+
line: line
334+
)
335+
}
319336
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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):
37+
if oldNode.id == node.id {
38+
return newNode
39+
} else {
40+
break
41+
}
42+
default:
43+
break
44+
}
45+
}
46+
return nil
47+
}
48+
49+
public override func visit(_ node: TokenSyntax) -> TokenSyntax {
50+
var modifiedNode = node
51+
for change in changes {
52+
switch change {
53+
case .replaceLeadingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
54+
modifiedNode = node.with(\.leadingTrivia, newTrivia)
55+
case .replaceTrailingTrivia(token: let changedNode, newTrivia: let newTrivia) where changedNode.id == node.id:
56+
modifiedNode = node.with(\.trailingTrivia, newTrivia)
57+
default:
58+
break
59+
}
60+
}
61+
return modifiedNode
62+
}
63+
64+
/// If `messages` is `nil`, applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree.
65+
/// If `messages` is not `nil`, applies only Fix-Its whose message is in `messages`.
66+
public static func applyFixes<T: SyntaxProtocol>(in diagnostics: [Diagnostic], withMessages messages: [String]?, to tree: T) -> Syntax {
67+
let applier = FixItApplier(diagnostics: diagnostics, withMessages: messages)
68+
return applier.rewrite(tree)
69+
}
70+
}

Tests/SwiftParserTest/Assertions.swift

Lines changed: 0 additions & 52 deletions
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

Lines changed: 57 additions & 3 deletions
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.
@@ -1092,6 +1120,33 @@ final class MacroSystemTests: XCTestCase {
10921120
)
10931121
}
10941122

1123+
func testAddCompletionHandlerWithNoAsync() {
1124+
assertMacroExpansion(
1125+
"""
1126+
@addCompletionHandler
1127+
func f(a: Int, for b: String, _ value: Double) -> String { }
1128+
""",
1129+
expandedSource: """
1130+
func f(a: Int, for b: String, _ value: Double) -> String { }
1131+
""",
1132+
diagnostics: [
1133+
DiagnosticSpec(
1134+
message: "can only add a completion-handler variant to an 'async' function",
1135+
line: 2,
1136+
column: 1,
1137+
fixIts: [
1138+
FixItSpec(message: "add 'async'")
1139+
]
1140+
)
1141+
],
1142+
macros: testMacros,
1143+
fixedSource: """
1144+
func f(a: Int, for b: String, _ value: Double) async -> String { }
1145+
""",
1146+
indentationWidth: indentationWidth
1147+
)
1148+
}
1149+
10951150
func testAddBackingStorage() {
10961151
assertMacroExpansion(
10971152
"""
@@ -1563,5 +1618,4 @@ final class MacroSystemTests: XCTestCase {
15631618
indentationWidth: indentationWidth
15641619
)
15651620
}
1566-
15671621
}

0 commit comments

Comments
 (0)