From e40d0d8a8132588fa6d3b2f6d5ef0659d2ad9c4a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 17 Sep 2023 12:28:33 -0700 Subject: [PATCH 1/5] Assert more with multiple trailing closures --- Package.resolved | 4 +- Package.swift | 5 +- README.md | 38 +- Sources/MacroTesting/AssertMacro.swift | 354 +++++++++++------- .../Documentation.docc/AssertMacro.md | 9 +- .../Documentation.docc/MacroTesting.md | 41 +- .../MacroTesting/Internal/Deprecations.swift | 100 +++++ .../AddAsyncCompletionHandlerTests.swift | 28 +- Tests/MacroTestingTests/AddBlockerTests.swift | 41 +- .../CaseDetectionMacroTests.swift | 2 +- .../CustomCodableMacroTests.swift | 2 +- .../DictionaryStorageMacroTests.swift | 2 +- Tests/MacroTestingTests/FailureTests.swift | 48 --- .../FontLiteralMacroTests.swift | 2 +- .../MetaEnumMacroTests.swift | 6 +- .../MacroTestingTests/NewTypeMacroTests.swift | 2 +- .../OptionSetMacroTests.swift | 2 +- .../StringifyMacroTests.swift | 2 +- Tests/MacroTestingTests/URLMacroTests.swift | 6 +- .../MacroTestingTests/WarningMacroTests.swift | 8 +- .../WrapStoredPropertiesMacroTests.swift | 2 +- 21 files changed, 437 insertions(+), 267 deletions(-) create mode 100644 Sources/MacroTesting/Internal/Deprecations.swift delete mode 100644 Tests/MacroTestingTests/FailureTests.swift diff --git a/Package.resolved b/Package.resolved index 6754c58..03b7e64 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "696b86a6d151578bca7c1a2a3ed419a5f834d40f", - "version" : "1.13.0" + "branch" : "inline-snapshot-migrations", + "revision" : "e7fe33e787632abff2f352cfef550d59cfaa5231" } }, { diff --git a/Package.swift b/Package.swift index b707d74..dd4c24c 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,10 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.13.0"), + .package( + url: "https://github.com/pointfreeco/swift-snapshot-testing", + branch: "inline-snapshot-migrations" + ), ], targets: [ .target( diff --git a/README.md b/README.md index c2241b7..29acec4 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ func testStringify() { """ #stringify(a + b) """ - } matches: { + } expansion: { """ (a + b, "a + b") """ @@ -82,14 +82,14 @@ assertMacro(["stringify": StringifyMacro.self], record: true) { """ #stringify(a + b) """ -} matches: { +} expansion: { """ (a + b, "a + b") """ } ``` -Now when you run the test again the freshest expanded macro will be written to the `matches` +Now when you run the test again the freshest expanded macro will be written to the `expansion` trailing closure. If you're writing many tests for a macro, you can avoid the repetitive work of specifying the macros @@ -111,7 +111,7 @@ class StringifyMacroTests: XCTestCase { """ #stringify(a + b) """ - } matches: { + } expansion: { """ (a + b, "a + b") """ @@ -138,27 +138,47 @@ override func invokeTest() { Macro Testing can also test diagnostics, such as warnings, errors, notes, and fix-its. When a macro expansion emits a diagnostic, it will render inline in the test. For example, a macro that adds completion handler functions to async functions may emit an error and fix-it when it is applied to a -non-async function. The resulting macro test will fully capture this information: +non-async function. The resulting macro test will fully capture this information, including where +the diagnostics are emitted, how the fix-its are applied, and how the final macro expands: ```swift func testNonAsyncFunctionDiagnostic() { assertMacro { """ @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { + func f(a: Int, for b: String) -> String { return b - } + } """ - } matches: { + } diagnostics: { """ @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { + func f(a: Int, for b: String) -> String { ┬─── ╰─ 🛑 can only add a completion-handler variant to an 'async' function ✏️ add 'async' return b } """ + } fixes: { + """ + @AddCompletionHandler + func f(a: Int, for b: String) async -> String { + return b + } + """ + } expansion: { + """ + func f(a: Int, for b: String) async -> String { + return b + } + + func f(a: Int, for b: String, completionHandler: @escaping (String) -> Void) { + Task { + completionHandler(await f(a: a, for: b, value)) + } + } + """ } } ``` diff --git a/Sources/MacroTesting/AssertMacro.swift b/Sources/MacroTesting/AssertMacro.swift index 7ddca42..19d12ed 100644 --- a/Sources/MacroTesting/AssertMacro.swift +++ b/Sources/MacroTesting/AssertMacro.swift @@ -32,7 +32,7 @@ import XCTest /// """ /// #stringify(a + b) /// """ -/// } matches: { +/// } expansion: { /// """ /// (a + b, "a + b") /// """ @@ -51,7 +51,7 @@ import XCTest /// let boolean: Bool /// } /// """ -/// } matches: { +/// } diagnostics: { /// """ /// @MetaEnum struct Cell { /// ┬──────── @@ -80,7 +80,7 @@ import XCTest /// > """ /// > #stringify(a + b) /// > """ -/// > } matches: { +/// > } expansion: { /// > """ /// > (a + b, "a + b") /// > """ @@ -91,12 +91,12 @@ import XCTest /// - Parameters: /// - macros: The macros to expand in the original source string. Required, either implicitly via /// ``withMacroTesting(isRecording:macros:operation:)-2vypn``, or explicitly via this parameter. -/// - applyFixIts: Applies fix-its to the original source and snapshots the result. If there are /// no fix-its to apply, or if any diagnostics are unfixable, the assertion will fail. /// - isRecording: Always records new snapshots when enabled. /// - originalSource: A string of Swift source code. -/// - expandedOrDiagnosedSource: The expected Swift source string with macros or diagnostics -/// expanded. +/// - diagnosedSource: Swift source code annotated with expected diagnostics. +/// - fixedSource: Swift source code with expected fix-its applied. +/// - expandedSource: Expected Swift source string with macros expanded. /// - file: The file where the assertion occurs. The default is the filename of the test case /// where you call this function. /// - function: The function where the assertion occurs. The default is the name of the test @@ -107,10 +107,11 @@ import XCTest /// function. public func assertMacro( _ macros: [String: Macro.Type]? = nil, - applyFixIts: Bool = false, record isRecording: Bool? = nil, of originalSource: () throws -> String, - matches expandedOrDiagnosedSource: (() -> String)? = nil, + diagnostics diagnosedSource: (() -> String)? = nil, + fixes fixedSource: (() -> String)? = nil, + expansion expandedSource: (() -> String)? = nil, file: StaticString = #filePath, function: StaticString = #function, line: UInt = #line, @@ -145,39 +146,210 @@ public func assertMacro( SnapshotTesting.isRecording = isRecording ?? MacroTestingConfiguration.current.isRecording defer { SnapshotTesting.isRecording = wasRecording } - assertInlineSnapshot( - of: try originalSource(), - as: .macroExpansion(macros, applyFixIts: applyFixIts, file: file, line: line), - message: """ - Actual output (\(actualPrefix)) differed from expected output (\(expectedPrefix)). \ - Difference: … - """, - syntaxDescriptor: InlineSnapshotSyntaxDescriptor( - trailingClosureLabel: "matches", - trailingClosureOffset: 1 - ), - matches: expandedOrDiagnosedSource, - file: file, - function: function, - line: line, - column: column - ) + do { + var origSourceFile = Parser.parse(source: try originalSource()) + if let foldedSourceFile = try OperatorTable.standardOperators.foldAll(origSourceFile).as( + SourceFileSyntax.self + ) { + origSourceFile = foldedSourceFile + } + + let origDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: origSourceFile) + let indentationWidth = Trivia( + stringLiteral: String( + SourceLocationConverter(fileName: "-", tree: origSourceFile).sourceLines + .first(where: { $0.first?.isWhitespace == true && $0 != "\n" })? + .prefix(while: { $0.isWhitespace }) + ?? " " + ) + ) + + var context = BasicMacroExpansionContext( + sourceFiles: [ + origSourceFile: .init(moduleName: "TestModule", fullFilePath: "Test.swift") + ] + ) + var expandedSourceFile = origSourceFile.expand( + macros: macros, + in: context, + indentationWidth: indentationWidth + ) + + var offset = 0 + + func anchor(_ diag: Diagnostic) -> Diagnostic { + let location = context.location(for: diag.position, anchoredAt: diag.node, fileName: "") + return Diagnostic( + node: diag.node, + position: AbsolutePosition(utf8Offset: location.offset), + message: diag.diagMessage, + highlights: diag.highlights, + notes: diag.notes, + fixIts: diag.fixIts + ) + } + + var allDiagnostics: [Diagnostic] { origDiagnostics + context.diagnostics } + if !allDiagnostics.isEmpty || diagnosedSource != nil { + offset += 1 + + let converter = SourceLocationConverter(fileName: "-", tree: origSourceFile) + let lineCount = converter.location(for: origSourceFile.endPosition).line + let diagnostics = + DiagnosticsFormatter + .annotatedSource( + tree: origSourceFile, + diags: allDiagnostics.map(anchor), + context: context, + contextSize: lineCount + ) + .description + .replacingOccurrences(of: #"(^|\n) *\d* +│ "#, with: "$1", options: .regularExpression) + .trimmingCharacters(in: .newlines) + + assertInlineSnapshot( + of: diagnostics, + as: ._lines, + message: """ + Diagnostic output (\(actualPrefix)) differed from expected output (\(expectedPrefix)). \ + Difference: … + """, + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + deprecatedTrailingClosureLabels: ["matches"], + trailingClosureLabel: "diagnostics", + trailingClosureOffset: offset + ), + matches: diagnosedSource, + file: file, + function: function, + line: line, + column: column + ) + } else if diagnosedSource != nil { + offset += 1 + InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "diagnostics", + trailingClosureOffset: offset + ) + .fail( + "Expected diagnostics, but there were none", + file: file, + line: line, + column: column + ) + } + + if !allDiagnostics.isEmpty && allDiagnostics.allSatisfy({ !$0.fixIts.isEmpty }) { + offset += 1 + + var fixedSourceFile = origSourceFile + fixedSourceFile = Parser.parse( + source: FixItApplier.applyFixes( + context: context, in: allDiagnostics.map(anchor), to: origSourceFile + ) + .description + ) + if let foldedSourceFile = try OperatorTable.standardOperators.foldAll(fixedSourceFile).as( + SourceFileSyntax.self + ) { + fixedSourceFile = foldedSourceFile + } + + assertInlineSnapshot( + of: fixedSourceFile.description.trimmingCharacters(in: .newlines), + as: ._lines, + message: """ + Fixed output (\(actualPrefix)) differed from expected output (\(expectedPrefix)). \ + Difference: … + """, + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "fixes", + trailingClosureOffset: offset + ), + matches: fixedSource, + file: file, + function: function, + line: line, + column: column + ) + + context = BasicMacroExpansionContext( + sourceFiles: [ + fixedSourceFile: .init(moduleName: "TestModule", fullFilePath: "Test.swift") + ] + ) + expandedSourceFile = fixedSourceFile.expand( + macros: macros, + in: context, + indentationWidth: indentationWidth + ) + } else if fixedSource != nil { + offset += 1 + InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "fixes", + trailingClosureOffset: offset + ) + .fail( + "Expected fix-its, but there were none", + file: file, + line: line, + column: column + ) + } + + if allDiagnostics.filter({ $0.diagMessage.severity == .error }).isEmpty + || expandedSource != nil + { + offset += 1 + assertInlineSnapshot( + of: expandedSourceFile.description.trimmingCharacters(in: .newlines), + as: ._lines, + message: """ + Expanded output (\(actualPrefix)) differed from expected output (\(expectedPrefix)). \ + Difference: … + """, + syntaxDescriptor: InlineSnapshotSyntaxDescriptor( + deprecatedTrailingClosureLabels: ["matches"], + trailingClosureLabel: "expansion", + trailingClosureOffset: offset + ), + matches: expandedSource, + file: file, + function: function, + line: line, + column: column + ) + } else if expandedSource != nil { + offset += 1 + InlineSnapshotSyntaxDescriptor( + trailingClosureLabel: "expansion", + trailingClosureOffset: offset + ) + .fail( + "Expected macro expansion, but there was none", + file: file, + line: line, + column: column + ) + } + } catch { + XCTFail("Threw error: \(error)", file: file, line: line) + } } /// Asserts that a given Swift source string matches an expected string with all macros expanded. /// -/// See ``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-3rrmp`` for more -/// details. +/// See ``assertMacro(_:record:of:diagnostics:fixes:expansion:file:function:line:column:)-6hxgm`` for +/// more details. /// /// - Parameters: /// - macros: The macros to expand in the original source string. Required, either implicitly via /// ``withMacroTesting(isRecording:macros:operation:)-2vypn``, or explicitly via this parameter. -/// - applyFixIts: Applies fix-its to the original source and snapshots the result. If there are -/// no fix-its to apply, or if any diagnostics are unfixable, the assertion will fail. /// - isRecording: Always records new snapshots when enabled. /// - originalSource: A string of Swift source code. -/// - expandedOrDiagnosedSource: The expected Swift source string with macros or diagnostics -/// expanded. +/// - diagnosedSource: Swift source code annotated with expected diagnostics. +/// - fixedSource: Swift source code with expected fix-its applied. +/// - expandedSource: Expected Swift source string with macros expanded. /// - file: The file where the assertion occurs. The default is the filename of the test case /// where you call this function. /// - function: The function where the assertion occurs. The default is the name of the test @@ -188,10 +360,11 @@ public func assertMacro( /// function. public func assertMacro( _ macros: [Macro.Type], - applyFixIts: Bool = false, record isRecording: Bool? = nil, of originalSource: () throws -> String, - matches expandedOrDiagnosedSource: (() -> String)? = nil, + diagnostics diagnosedSource: (() -> String)? = nil, + fixes fixedSource: (() -> String)? = nil, + expansion expandedSource: (() -> String)? = nil, file: StaticString = #filePath, function: StaticString = #function, line: UInt = #line, @@ -199,10 +372,11 @@ public func assertMacro( ) { assertMacro( Dictionary(macros: macros), - applyFixIts: applyFixIts, record: isRecording, of: originalSource, - matches: expandedOrDiagnosedSource, + diagnostics: diagnosedSource, + fixes: fixedSource, + expansion: expandedSource, file: file, function: function, line: line, @@ -373,94 +547,6 @@ extension Snapshotting where Value == String, Format == String { ) } -extension Snapshotting where Value == String, Format == String { - fileprivate static func macroExpansion( - _ macros: [String: Macro.Type], - applyFixIts: Bool, - testModuleName: String = "TestModule", - testFileName: String = "Test.swift", - indentationWidth: Trivia? = nil, - file: StaticString = #filePath, - line: UInt = #line - ) -> Self { - Snapshotting._lines.pullback { input in - var origSourceFile = Parser.parse(source: input) - if let foldedSourceFile = try? OperatorTable.standardOperators.foldAll(origSourceFile).as( - SourceFileSyntax.self - ) { - origSourceFile = foldedSourceFile - } - - let origDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: origSourceFile) - - let context = BasicMacroExpansionContext( - sourceFiles: [ - origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName) - ] - ) - let indentationWidth = - indentationWidth - ?? Trivia( - stringLiteral: String( - SourceLocationConverter(fileName: "-", tree: origSourceFile).sourceLines - .first(where: { $0.first?.isWhitespace == true && $0 != "\n" })? - .prefix(while: { $0.isWhitespace }) - ?? " " - ) - ) - let expandedSourceFile = origSourceFile.expand( - macros: macros, - in: context, - indentationWidth: indentationWidth - ) - - guard - origDiagnostics.isEmpty, - context.diagnostics.isEmpty - else { - let allDiagnostics = origDiagnostics + context.diagnostics - - func anchor(_ diag: Diagnostic) -> Diagnostic { - let location = context.location(for: diag.position, anchoredAt: diag.node, fileName: "") - return Diagnostic( - node: diag.node, - position: AbsolutePosition(utf8Offset: location.offset), - message: diag.diagMessage, - highlights: diag.highlights, - notes: diag.notes, - fixIts: diag.fixIts - ) - } - - let unfixableDiagnostics = allDiagnostics.filter { $0.fixIts.isEmpty } - if applyFixIts, unfixableDiagnostics.isEmpty { - let fixedSourceFile = FixItApplier.applyFixes( - context: context, in: allDiagnostics.map(anchor), to: origSourceFile - ) - return diagnosticsOrTree(context: context, diags: [], tree: fixedSourceFile) - } else { - if applyFixIts { - XCTFail("Not all diagnostics are fixable.", file: file, line: line) - } - return diagnosticsOrTree( - context: context, diags: allDiagnostics.map(anchor), tree: origSourceFile - ) - } - } - - if applyFixIts { - XCTFail("No fix-its to apply.", file: file, line: line) - } - - return diagnosticsOrTree( - context: context, - diags: ParseDiagnosticsGenerator.diagnostics(for: expandedSourceFile), - tree: expandedSourceFile - ) - } - } -} - internal func macroName(className: String, isExpression: Bool) -> String { var name = className @@ -475,25 +561,7 @@ internal func macroName(className: String, isExpression: Bool) -> String { return name } -private func diagnosticsOrTree( - context: BasicMacroExpansionContext, - diags: [Diagnostic], - tree: some SyntaxProtocol -) -> String { - guard !diags.isEmpty - else { return tree.description.trimmingCharacters(in: .newlines) } - - let converter = SourceLocationConverter(fileName: "-", tree: tree) - let lineCount = converter.location(for: tree.endPosition).line - return - DiagnosticsFormatter - .annotatedSource(tree: tree, diags: diags, context: context, contextSize: lineCount) - .description - .replacingOccurrences(of: #"(^|\n) *\d* +│ "#, with: "$1", options: .regularExpression) - .trimmingCharacters(in: .newlines) -} - -private struct MacroTestingConfiguration { +struct MacroTestingConfiguration { @TaskLocal static var current = Self() var isRecording = false @@ -501,7 +569,7 @@ private struct MacroTestingConfiguration { } extension Dictionary where Key == String, Value == Macro.Type { - fileprivate init(macros: [Macro.Type]) { + init(macros: [Macro.Type]) { self.init( macros.map { let name = macroName( diff --git a/Sources/MacroTesting/Documentation.docc/AssertMacro.md b/Sources/MacroTesting/Documentation.docc/AssertMacro.md index 6b506ea..55fec09 100644 --- a/Sources/MacroTesting/Documentation.docc/AssertMacro.md +++ b/Sources/MacroTesting/Documentation.docc/AssertMacro.md @@ -1,7 +1,12 @@ -# ``MacroTesting/assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-3rrmp`` +# ``MacroTesting/assertMacro(_:record:of:diagnostics:fixes:expansion:file:function:line:column:)-6hxgm`` ## Topics ### Overloads -- ``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-7spc6`` +- ``assertMacro(_:record:of:diagnostics:fixes:expansion:file:function:line:column:)-3hjp`` + +### Deprecations + +- ``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-4xamb`` +- ``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-7jwrb`` diff --git a/Sources/MacroTesting/Documentation.docc/MacroTesting.md b/Sources/MacroTesting/Documentation.docc/MacroTesting.md index d1617af..3d62641 100644 --- a/Sources/MacroTesting/Documentation.docc/MacroTesting.md +++ b/Sources/MacroTesting/Documentation.docc/MacroTesting.md @@ -35,7 +35,7 @@ func testStringify() { """ #stringify(a + b) """ - } matches: { + } expansion: { """ (a + b, "a + b") """ @@ -57,21 +57,20 @@ running the test again will produce a nicely formatted message: You can even have the library automatically re-record the macro expansion directly into your test file by providing the `record` argument to -``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-3rrmp: - +``assertMacro(_:record:of:diagnostics:fixes:expansion:file:function:line:column:)-6hxgm`` ```swift assertMacro(["stringify": StringifyMacro.self], record: true) { """ #stringify(a + b) """ -} matches: { +} expansion: { """ (a + b, "a + b") """ } ``` -Now when you run the test again the freshest expanded macro will be written to the `matches` +Now when you run the test again the freshest expanded macro will be written to the `expansion` trailing closure. If you're writing many tests for a macro, you can avoid the repetitive work of specifying the macros @@ -93,7 +92,7 @@ class StringifyMacroTests: XCTestCase { """ #stringify(a + b) """ - } matches: { + } expansion: { """ (a + b, "a + b") """ @@ -121,27 +120,47 @@ override func invokeTest() { Macro Testing can also test diagnostics, such as warnings, errors, notes, and fix-its. When a macro expansion emits a diagnostic, it will render inline in the test. For example, a macro that adds completion handler functions to async functions may emit an error and fix-it when it is applied to a -non-async function. The resulting macro test will fully capture this information: +non-async function. The resulting macro test will fully capture this information, including where +the diagnostics are emitted, how the fix-its are applied, and how the final macro expands: ```swift func testNonAsyncFunctionDiagnostic() { assertMacro { """ @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { + func f(a: Int, for b: String) -> String { return b } """ - } matches: { + } diagnostics: { """ @AddCompletionHandler - func f(a: Int, for b: String, _ value: Double) -> String { + func f(a: Int, for b: String) -> String { ┬─── ╰─ 🛑 can only add a completion-handler variant to an 'async' function ✏️ add 'async' return b } """ + } fixes: { + """ + @AddCompletionHandler + func f(a: Int, for b: String) async -> String { + return b + } + """ + } expansion: { + """ + func f(a: Int, for b: String) async -> String { + return b + } + + func f(a: Int, for b: String, completionHandler: @escaping (String) -> Void) { + Task { + completionHandler(await f(a: a, for: b, value)) + } + } + """ } } ``` @@ -150,5 +169,5 @@ func testNonAsyncFunctionDiagnostic() { ### Essentials -- ``assertMacro(_:applyFixIts:record:of:matches:file:function:line:column:)-3rrmp`` +- ``assertMacro(_:record:of:diagnostics:fixes:expansion:file:function:line:column:)-6hxgm`` - ``withMacroTesting(isRecording:macros:operation:)-2vypn`` diff --git a/Sources/MacroTesting/Internal/Deprecations.swift b/Sources/MacroTesting/Internal/Deprecations.swift new file mode 100644 index 0000000..bfcbb68 --- /dev/null +++ b/Sources/MacroTesting/Internal/Deprecations.swift @@ -0,0 +1,100 @@ +import InlineSnapshotTesting +import SwiftDiagnostics +import SwiftOperators +import SwiftParser +import SwiftParserDiagnostics +import SwiftSyntax +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros +import XCTest + +// MARK: Deprecated after 0.1.0 + +@available(*, deprecated, message: "Re-record this assertion") +public func assertMacro( + _ macros: [String: Macro.Type]? = nil, + record isRecording: Bool? = nil, + of originalSource: () throws -> String, + matches expandedOrDiagnosedSource: () -> String, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + guard isRecording ?? MacroTestingConfiguration.current.isRecording else { + XCTFail("Re-record this assertion", file: file, line: line) + return + } + assertMacro( + macros, + record: true, + of: originalSource, + file: file, + function: function, + line: line, + column: column + ) +} + +@available(*, deprecated, message: "Re-record this assertion") +public func assertMacro( + _ macros: [Macro.Type], + record isRecording: Bool? = nil, + of originalSource: () throws -> String, + matches expandedOrDiagnosedSource: () -> String, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + assertMacro( + Dictionary(macros: macros), + record: isRecording, + of: originalSource, + matches: expandedOrDiagnosedSource, + file: file, + function: function, + line: line, + column: column + ) +} + +@available(*, deprecated, message: "Delete 'matches' and re-record this assertion") +public func assertMacro( + _ macros: [String: Macro.Type]? = nil, + applyFixIts: Bool, + record isRecording: Bool? = nil, + of originalSource: () throws -> String, + matches expandedOrDiagnosedSource: () -> String, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + XCTFail("Delete 'matches' and re-record this assertion", file: file, line: line) +} + +@available(*, deprecated, message: "Delete 'matches' and re-record this assertion") +public func assertMacro( + _ macros: [Macro.Type], + applyFixIts: Bool, + record isRecording: Bool? = nil, + of originalSource: () throws -> String, + matches expandedOrDiagnosedSource: () -> String, + file: StaticString = #filePath, + function: StaticString = #function, + line: UInt = #line, + column: UInt = #column +) { + assertMacro( + Dictionary(macros: macros), + applyFixIts: applyFixIts, + record: isRecording, + of: originalSource, + matches: expandedOrDiagnosedSource, + file: file, + function: function, + line: line, + column: column + ) +} diff --git a/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift b/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift index 33f684a..868aa4e 100644 --- a/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift +++ b/Tests/MacroTestingTests/AddAsyncCompletionHandlerTests.swift @@ -28,7 +28,7 @@ final class AddAsyncCompletionHandlerMacroTests: BaseTestCase { } } """# - } matches: { + } expansion: { #""" struct MyStruct { func f(a: Int, for b: String, _ value: Double) async -> String { @@ -82,7 +82,7 @@ final class AddAsyncCompletionHandlerMacroTests: BaseTestCase { @AddCompletionHandler struct Foo {} """ - } matches: { + } diagnostics: { """ @AddCompletionHandler ┬──────────────────── @@ -93,15 +93,14 @@ final class AddAsyncCompletionHandlerMacroTests: BaseTestCase { } func testNonAsyncFunctionDiagnostic() { - let source = """ + assertMacro { + """ @AddCompletionHandler func f(a: Int, for b: String, _ value: Double) -> String { return b } """ - assertMacro { - source - } matches: { + } diagnostics: { """ @AddCompletionHandler func f(a: Int, for b: String, _ value: Double) -> String { @@ -111,16 +110,25 @@ final class AddAsyncCompletionHandlerMacroTests: BaseTestCase { return b } """ - } - assertMacro(applyFixIts: true) { - source - } matches: { + } fixes: { """ @AddCompletionHandler func f(a: Int, for b: String, _ value: Double) async -> String { return b } """ + } expansion: { + """ + func f(a: Int, for b: String, _ value: Double) async -> String { + return b + } + + func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { + Task { + completionHandler(await f(a: a, for: b, value)) + } + } + """ } } } diff --git a/Tests/MacroTestingTests/AddBlockerTests.swift b/Tests/MacroTestingTests/AddBlockerTests.swift index 7c54cdb..69b9dbc 100644 --- a/Tests/MacroTestingTests/AddBlockerTests.swift +++ b/Tests/MacroTestingTests/AddBlockerTests.swift @@ -9,15 +9,14 @@ final class AddBlockerTests: BaseTestCase { } func testAddBlocker() { - let source = """ + assertMacro { + """ let x = 1 let y = 2 let z = 3 #addBlocker(x * y + z) """ - assertMacro { - source - } matches: { + } diagnostics: { """ let x = 1 let y = 2 @@ -27,48 +26,40 @@ final class AddBlockerTests: BaseTestCase { ╰─ ⚠️ blocked an add; did you mean to subtract? ✏️ use '-' """ - } - assertMacro(applyFixIts: true) { - source - } matches: { + } fixes: { """ let x = 1 let y = 2 let z = 3 #addBlocker(x * y - z) """ + } expansion: { + """ + let x = 1 + let y = 2 + let z = 3 + x * y - z + """ } } func testAddBlocker_Inline() { - let source = """ + assertMacro { + """ #addBlocker(1 * 2 + 3) """ - assertMacro { - source - } matches: { + } diagnostics: { """ #addBlocker(1 * 2 + 3) ───── ┬ ─ ╰─ ⚠️ blocked an add; did you mean to subtract? ✏️ use '-' """ - } - assertMacro(applyFixIts: true) { - source - } matches: { - """ - #addBlocker(1 * 2 - 3) - """ - } - } - - func testAddBlocker_Expanded() { - assertMacro { + } fixes: { """ #addBlocker(1 * 2 - 3) """ - } matches: { + } expansion: { """ 1 * 2 - 3 """ diff --git a/Tests/MacroTestingTests/CaseDetectionMacroTests.swift b/Tests/MacroTestingTests/CaseDetectionMacroTests.swift index ea29a40..ffd0747 100644 --- a/Tests/MacroTestingTests/CaseDetectionMacroTests.swift +++ b/Tests/MacroTestingTests/CaseDetectionMacroTests.swift @@ -19,7 +19,7 @@ final class CaseDetectionMacroTests: BaseTestCase { case snake } """# - } matches: { + } expansion: { """ enum Pet { case dog diff --git a/Tests/MacroTestingTests/CustomCodableMacroTests.swift b/Tests/MacroTestingTests/CustomCodableMacroTests.swift index 914bf0a..c1dc24d 100644 --- a/Tests/MacroTestingTests/CustomCodableMacroTests.swift +++ b/Tests/MacroTestingTests/CustomCodableMacroTests.swift @@ -19,7 +19,7 @@ final class CustomCodableMacroTests: BaseTestCase { func randomFunction() {} } """ - } matches: { + } expansion: { """ struct CustomCodableString: Codable { var propertyWithOtherName: String diff --git a/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift b/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift index 729804a..906f7e3 100644 --- a/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift +++ b/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift @@ -17,7 +17,7 @@ final class DictionaryStorageMacroTests: BaseTestCase { var y: Int = 2 } """ - } matches: { + } expansion: { """ struct Point { var x: Int = 1 { diff --git a/Tests/MacroTestingTests/FailureTests.swift b/Tests/MacroTestingTests/FailureTests.swift deleted file mode 100644 index aa15063..0000000 --- a/Tests/MacroTestingTests/FailureTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) || os(watchOS) - import MacroTesting - import SwiftSyntaxMacros - import XCTest - - final class FailureTests: BaseTestCase { - func testApplyFixIts_NoFixIts() { - struct MyMacro: Macro {} - XCTExpectFailure { - $0.compactDescription == "failed - No fix-its to apply." - } - assertMacro(["MyMacro": MyMacro.self], applyFixIts: true) { - """ - let foo = "bar" - """ - } matches: { - """ - let foo = "bar" - """ - } - } - - func testApplyFixIts_Unfixable() { - XCTExpectFailure { - $0.compactDescription == "failed - Not all diagnostics are fixable." - } - assertMacro(["MetaEnum": MetaEnumMacro.self], applyFixIts: true) { - """ - @MetaEnum struct Cell { - let integer: Int - let text: String - let boolean: Bool - } - """ - } matches: { - """ - @MetaEnum struct Cell { - ┬──────── - ╰─ 🛑 '@MetaEnum' can only be attached to an enum, not a struct - let integer: Int - let text: String - let boolean: Bool - } - """ - } - } - } -#endif diff --git a/Tests/MacroTestingTests/FontLiteralMacroTests.swift b/Tests/MacroTestingTests/FontLiteralMacroTests.swift index b7574e8..9712cb3 100644 --- a/Tests/MacroTestingTests/FontLiteralMacroTests.swift +++ b/Tests/MacroTestingTests/FontLiteralMacroTests.swift @@ -18,7 +18,7 @@ final class FontLiteralMacroTests: BaseTestCase { let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin) """ - } matches: { + } expansion: { """ struct Font: ExpressibleByFontLiteral { init(fontLiteralName: String, size: Int, weight: MacroExamplesLib.FontWeight) { diff --git a/Tests/MacroTestingTests/MetaEnumMacroTests.swift b/Tests/MacroTestingTests/MetaEnumMacroTests.swift index d6538a8..d9f76cd 100644 --- a/Tests/MacroTestingTests/MetaEnumMacroTests.swift +++ b/Tests/MacroTestingTests/MetaEnumMacroTests.swift @@ -18,7 +18,7 @@ final class MetaEnumMacroTests: BaseTestCase { case null } """# - } matches: { + } expansion: { """ enum Value { case integer(Int) @@ -58,7 +58,7 @@ final class MetaEnumMacroTests: BaseTestCase { case boolean(Bool) } """ - } matches: { + } expansion: { """ public enum Cell { case integer(Int) @@ -94,7 +94,7 @@ final class MetaEnumMacroTests: BaseTestCase { let boolean: Bool } """ - } matches: { + } diagnostics: { """ @MetaEnum struct Cell { ┬──────── diff --git a/Tests/MacroTestingTests/NewTypeMacroTests.swift b/Tests/MacroTestingTests/NewTypeMacroTests.swift index 6429ec8..fa0370d 100644 --- a/Tests/MacroTestingTests/NewTypeMacroTests.swift +++ b/Tests/MacroTestingTests/NewTypeMacroTests.swift @@ -15,7 +15,7 @@ final class NewTypeMacroTests: BaseTestCase { public struct MyString { } """ - } matches: { + } expansion: { """ public struct MyString { diff --git a/Tests/MacroTestingTests/OptionSetMacroTests.swift b/Tests/MacroTestingTests/OptionSetMacroTests.swift index bbf8eec..ab34c39 100644 --- a/Tests/MacroTestingTests/OptionSetMacroTests.swift +++ b/Tests/MacroTestingTests/OptionSetMacroTests.swift @@ -24,7 +24,7 @@ final class OptionSetMacroTests: BaseTestCase { static let all: ShippingOptions = [.express, .priority, .standard] } """ - } matches: { + } expansion: { """ struct ShippingOptions { private enum Options: Int { diff --git a/Tests/MacroTestingTests/StringifyMacroTests.swift b/Tests/MacroTestingTests/StringifyMacroTests.swift index 1534d61..f82e87f 100644 --- a/Tests/MacroTestingTests/StringifyMacroTests.swift +++ b/Tests/MacroTestingTests/StringifyMacroTests.swift @@ -15,7 +15,7 @@ final class StringifyMacroTests: BaseTestCase { let y = 2 print(#stringify(x + y)) """# - } matches: { + } expansion: { """ let x = 1 let y = 2 diff --git a/Tests/MacroTestingTests/URLMacroTests.swift b/Tests/MacroTestingTests/URLMacroTests.swift index a149298..c5ff289 100644 --- a/Tests/MacroTestingTests/URLMacroTests.swift +++ b/Tests/MacroTestingTests/URLMacroTests.swift @@ -13,7 +13,7 @@ final class URLMacroTests: BaseTestCase { #""" print(#URL("https://swift.org/")) """# - } matches: { + } expansion: { """ print(URL(string: "https://swift.org/")!) """ @@ -26,7 +26,7 @@ final class URLMacroTests: BaseTestCase { let domain = "domain.com" print(#URL("https://\(domain)/api/path")) """# - } matches: { + } diagnostics: { #""" let domain = "domain.com" print(#URL("https://\(domain)/api/path")) @@ -41,7 +41,7 @@ final class URLMacroTests: BaseTestCase { #""" print(#URL("https://not a url.com")) """# - } matches: { + } diagnostics: { """ print(#URL("https://not a url.com")) ┬──────────────────────────── diff --git a/Tests/MacroTestingTests/WarningMacroTests.swift b/Tests/MacroTestingTests/WarningMacroTests.swift index 6e2b291..dea3088 100644 --- a/Tests/MacroTestingTests/WarningMacroTests.swift +++ b/Tests/MacroTestingTests/WarningMacroTests.swift @@ -13,12 +13,16 @@ final class WarningMacroTests: BaseTestCase { #""" #myWarning("remember to pass a string literal here") """# - } matches: { + } diagnostics: { """ #myWarning("remember to pass a string literal here") ┬─────────────────────────────────────────────────── ╰─ ⚠️ remember to pass a string literal here """ + } expansion: { + """ + () + """ } } @@ -28,7 +32,7 @@ final class WarningMacroTests: BaseTestCase { let text = "oops" #myWarning(text) """ - } matches: { + } diagnostics: { """ let text = "oops" #myWarning(text) diff --git a/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift b/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift index cf20f5f..2fde6b0 100644 --- a/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift +++ b/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift @@ -16,7 +16,7 @@ final class WrapStoredPropertiesMacroTests: BaseTestCase { var x: Int } """ - } matches: { + } expansion: { """ struct OldStorage { @available(*, deprecated, message: "hands off my data") From 161d891d91823b12f6a5dce37ea2c8faf56b0b65 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 23 Sep 2023 08:27:49 -0700 Subject: [PATCH 2/5] Update Deprecations.swift --- Sources/MacroTesting/Internal/Deprecations.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MacroTesting/Internal/Deprecations.swift b/Sources/MacroTesting/Internal/Deprecations.swift index bfcbb68..a323908 100644 --- a/Sources/MacroTesting/Internal/Deprecations.swift +++ b/Sources/MacroTesting/Internal/Deprecations.swift @@ -59,7 +59,7 @@ public func assertMacro( ) } -@available(*, deprecated, message: "Delete 'matches' and re-record this assertion") +@available(*, deprecated, message: "Delete 'applyFixIts' and 'matches' and re-record this assertion") public func assertMacro( _ macros: [String: Macro.Type]? = nil, applyFixIts: Bool, @@ -74,7 +74,7 @@ public func assertMacro( XCTFail("Delete 'matches' and re-record this assertion", file: file, line: line) } -@available(*, deprecated, message: "Delete 'matches' and re-record this assertion") +@available(*, deprecated, message: "Delete 'applyFixIts' and 'matches' and re-record this assertion") public func assertMacro( _ macros: [Macro.Type], applyFixIts: Bool, From f1b71614c5cac90e75f5ebb95977cfbc90a2711a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 2 Oct 2023 14:36:32 -0700 Subject: [PATCH 3/5] fix --- .../Diagnostic+UnderlineHighlights.swift | 6 ++++- .../DiagnosticsFormatter.swift | 3 ++- .../DictionaryStorageMacroTests.swift | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Sources/MacroTesting/Internal/Diagnostic+UnderlineHighlights.swift b/Sources/MacroTesting/Internal/Diagnostic+UnderlineHighlights.swift index 69fa0cc..e216045 100644 --- a/Sources/MacroTesting/Internal/Diagnostic+UnderlineHighlights.swift +++ b/Sources/MacroTesting/Internal/Diagnostic+UnderlineHighlights.swift @@ -4,6 +4,7 @@ import SwiftSyntaxMacroExpansion extension Array where Element == Diagnostic { func underlineHighlights( + sourceString: String, lineNumber: Int, column: Int, context: BasicMacroExpansionContext @@ -18,7 +19,10 @@ extension Array where Element == Diagnostic { let endLocation = context.location( for: highlight.endPositionBeforeTrailingTrivia, anchoredAt: diag.node, fileName: "" ) - guard startLocation.line == lineNumber, startLocation.line == endLocation.line + guard + startLocation.line == lineNumber, + startLocation.line == endLocation.line, + sourceString.contains(diag.node.trimmedDescription) else { continue } partialResult.highlightColumns.formUnion(startLocation.column.. Date: Mon, 2 Oct 2023 14:47:33 -0700 Subject: [PATCH 4/5] bump --- Package.resolved | 4 ++-- Package.swift | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 03b7e64..118c415 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "branch" : "inline-snapshot-migrations", - "revision" : "e7fe33e787632abff2f352cfef550d59cfaa5231" + "revision" : "879016eda0ab441f060b7117d7861032a0f3a3c4", + "version" : "0.14.0" } }, { diff --git a/Package.swift b/Package.swift index dd4c24c..1c3ec74 100644 --- a/Package.swift +++ b/Package.swift @@ -18,10 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), - .package( - url: "https://github.com/pointfreeco/swift-snapshot-testing", - branch: "inline-snapshot-migrations" - ), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "0.14.0"), ], targets: [ .target( From 099098cf655e93793103175e33e04b99259b0726 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 2 Oct 2023 15:02:23 -0700 Subject: [PATCH 5/5] wip --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 118c415..4b5970f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "879016eda0ab441f060b7117d7861032a0f3a3c4", - "version" : "0.14.0" + "revision" : "506b6052384d8e97a4bb16fe8680325351c23c64", + "version" : "1.14.0" } }, { diff --git a/Package.swift b/Package.swift index 1c3ec74..f09da29 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "0.14.0"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.14.0"), ], targets: [ .target(