diff --git a/Package.swift b/Package.swift index 48137482a52..59c23fe321c 100644 --- a/Package.swift +++ b/Package.swift @@ -56,6 +56,7 @@ let package = Package( .library(name: "SwiftSyntax", type: .static, targets: ["SwiftSyntax"]), .library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]), .library(name: "SwiftSyntaxMacros", type: .static, targets: ["SwiftSyntaxMacros"]), + .library(name: "SwiftSyntaxMacrosTestSupport", type: .static, targets: ["SwiftSyntaxMacrosTestSupport"]), ], targets: [ // MARK: - Internal helper targets @@ -178,7 +179,14 @@ let package = Package( .testTarget( name: "SwiftSyntaxMacrosTest", - dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntaxBuilder", "SwiftSyntaxMacros"] + dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftSyntaxMacrosTestSupport"] + ), + + // MARK: SwiftSyntaxMacrosTestSupport + + .target( + name: "SwiftSyntaxMacrosTestSupport", + dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftParser", "SwiftSyntaxMacros"] ), // MARK: SwiftParser diff --git a/Sources/SwiftBasicFormat/BasicFormat.swift b/Sources/SwiftBasicFormat/BasicFormat.swift index 4f4c6847a95..43ad368d5d0 100644 --- a/Sources/SwiftBasicFormat/BasicFormat.swift +++ b/Sources/SwiftBasicFormat/BasicFormat.swift @@ -216,6 +216,7 @@ open class BasicFormat: SyntaxRewriter { (.regexLiteralPattern, _), (.regexSlash, .extendedRegexDelimiter), // closing extended regex delimiter should never be separate by a space (.rightAngle, .leftParen), // func foo(x: T) + (.rightBrace, .leftParen), // { return 1 }() (.rightParen, .leftParen), // returnsClosure()() (.rightParen, .period), // foo().bar (.rightSquareBracket, .period), // myArray[1].someProperty @@ -310,6 +311,14 @@ open class BasicFormat: SyntaxRewriter { return false }() + lazy var nextTokenWillStartWithWhitespace: Bool = { + guard let nextToken = nextToken else { + return false + } + return nextToken.leadingTrivia.startsWithWhitespace + || (requiresLeadingNewline(nextToken) && isMutable(nextToken)) + }() + lazy var nextTokenWillStartWithNewline: Bool = { guard let nextToken = nextToken else { return false @@ -359,7 +368,7 @@ open class BasicFormat: SyntaxRewriter { // because newlines should be preferred to spaces as a whitespace if requiresWhitespace(between: token, and: nextToken) && !trailingTrivia.endsWithWhitespace - && !nextTokenWillStartWithNewline + && !nextTokenWillStartWithWhitespace { trailingTrivia += .space } diff --git a/Sources/SwiftSyntaxMacros/MacroSystem.swift b/Sources/SwiftSyntaxMacros/MacroSystem.swift index 1c3bf921639..ebdfbfcebfe 100644 --- a/Sources/SwiftSyntaxMacros/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacros/MacroSystem.swift @@ -200,7 +200,7 @@ class MacroApplication: SyntaxRewriter { } ) } catch { - context.addDiagnostics(from: error, node: node) + context.addDiagnostics(from: error, node: declExpansion) } continue diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift new file mode 100644 index 00000000000..fd899420359 --- /dev/null +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -0,0 +1,305 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import _SwiftSyntaxTestSupport +import SwiftBasicFormat +import SwiftDiagnostics +import SwiftParser +import SwiftSyntax +import SwiftSyntaxMacros +import XCTest + +private extension String { + // This implementation is really slow; to use it outside a test it should be optimized. + func trimmingTrailingWhitespace() -> String { + return self.replacingOccurrences(of: "[ ]+\\n", with: "\n", options: .regularExpression) + } +} + +// MARK: - Note + +/// Describes a diagnostic note that tests expect to be created by a macro expansion. +public struct NoteSpec { + /// The expected message of the note + public let message: String + + /// The line to which the note is expected to point + public let line: Int + + /// The column to which the note is expected to point + public let column: Int + + /// The file and line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + internal let originatorFile: StaticString + internal let originatorLine: UInt + + /// Creates a new `NoteSpec` that describes a note tests are expecting to be generated by a macro expansion. + /// + /// - Parameters: + /// - message: The expected message of the note + /// - line: The line to which the note is expected to point + /// - column: The column to which the note is expected to point + /// - originatorFile: The file at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + /// - originatorLine: The line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + public init( + message: String, + line: Int, + column: Int, + originatorFile: StaticString = #file, + originatorLine: UInt = #line + ) { + self.message = message + self.line = line + self.column = column + self.originatorFile = originatorFile + self.originatorLine = originatorLine + } +} + +func assertNote( + _ note: Note, + in tree: T, + expected spec: NoteSpec +) { + assertStringsEqualWithDiff(note.message, spec.message, "message of note does not match", file: spec.originatorFile, line: spec.originatorLine) + let location = note.location(converter: SourceLocationConverter(file: "", source: tree.description)) + XCTAssertEqual(location.line, spec.line, "line of note does not match", file: spec.originatorFile, line: spec.originatorLine) + XCTAssertEqual(location.column, spec.column, "column of note does not match", file: spec.originatorFile, line: spec.originatorLine) +} + +// MARK: - Fix-It + +/// Describes a Fix-It that tests expect to be created by a macro expansion. +/// +/// Currently, it only compares the message of the Fix-It. In the future, it might +/// also compare the expected changes that should be performed by the Fix-It. +public struct FixItSpec { + /// The expected message of the Fix-It + public let message: String + + /// The file and line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + internal let originatorFile: StaticString + internal let originatorLine: UInt + + /// Creates a new `FixItSpec` that describes a Fix-It tests are expecting to be generated by a macro expansion. + /// + /// - Parameters: + /// - message: The expected message of the note + /// - originatorFile: The file at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + /// - originatorLine: The line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + public init( + message: String, + originatorFile: StaticString = #file, + originatorLine: UInt = #line + ) { + self.message = message + self.originatorFile = originatorFile + self.originatorLine = originatorLine + } +} + +func assertFixIt( + _ fixIt: FixIt, + expected spec: FixItSpec +) { + assertStringsEqualWithDiff(fixIt.message.message, spec.message, "message of Fix-It does not match", file: spec.originatorFile, line: spec.originatorLine) +} + +// MARK: - Diagnostic + +/// Describes a diagnostic that tests expect to be created by a macro expansion. +public struct DiagnosticSpec { + /// If not `nil`, the ID, which the diagnostic is expected to have. + public let id: MessageID? + + /// The expected message of the diagnostic + public let message: String + + /// The line to which the diagnostic is expected to point + public let line: Int + + /// The column to which the diagnostic is expected to point + public let column: Int + + /// The expected severity of the diagnostic + public let severity: DiagnosticSeverity + + /// If not `nil`, the text the diagnostic is expected to highlight + public let highlight: String? + + /// The notes that are expected to be attached to the diagnostic + public let notes: [NoteSpec] + + /// The messages of the Fix-Its the diagnostic is expected to produce + public let fixIts: [FixItSpec] + + /// The file and line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + internal let originatorFile: StaticString + internal let originatorLine: UInt + + /// Creates a new `DiagnosticSpec` that describes a diagnsotic tests are expecting to be generated by a macro expansion. + /// + /// - Parameters: + /// - id: If not `nil`, the ID, which the diagnostic is expected to have. + /// - message: The expected message of the diagnostic + /// - line: The line to which the diagnostic is expected to point + /// - column: The column to which the diagnostic is expected to point + /// - severity: The expected severity of the diagnostic + /// - highlight: If not `nil`, the text the diagnostic is expected to highlight + /// - notes: The notes that are expected to be attached to the diagnostic + /// - fixIts: The messages of the Fix-Its the diagnostic is expected to produce + /// - originatorFile: The file at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + /// - originatorLine: The line at which this `NoteSpec` was created, so that assertion failures can be reported at its location. + public init( + id: MessageID? = nil, + message: String, + line: Int, + column: Int, + severity: DiagnosticSeverity = .error, + highlight: String? = nil, + notes: [NoteSpec] = [], + fixIts: [FixItSpec] = [], + originatorFile: StaticString = #file, + originatorLine: UInt = #line + ) { + self.id = id + self.message = message + self.line = line + self.column = column + self.severity = severity + self.highlight = highlight + self.notes = notes + self.fixIts = fixIts + self.originatorFile = originatorFile + self.originatorLine = originatorLine + } +} + +func assertDiagnostic( + _ diag: Diagnostic, + in tree: T, + expected spec: DiagnosticSpec +) { + if let id = spec.id { + XCTAssertEqual(diag.diagnosticID, id, "diagnostic ID does not match", file: spec.originatorFile, line: spec.originatorLine) + } + assertStringsEqualWithDiff(diag.message, spec.message, "message does not match", file: spec.originatorFile, line: spec.originatorLine) + let location = diag.location(converter: SourceLocationConverter(file: "", source: tree.description)) + XCTAssertEqual(location.line, spec.line, "line does not match", file: spec.originatorFile, line: spec.originatorLine) + XCTAssertEqual(location.column, spec.column, "column does not match", file: spec.originatorFile, line: spec.originatorLine) + + XCTAssertEqual(spec.severity, diag.diagMessage.severity, "severity does not match", file: spec.originatorFile, line: spec.originatorLine) + + if let highlight = spec.highlight { + var highlightedCode = "" + highlightedCode.append(diag.highlights.first?.with(\.leadingTrivia, []).description ?? "") + for highlight in diag.highlights.dropFirst().dropLast() { + highlightedCode.append(highlight.description) + } + if diag.highlights.count > 1 { + highlightedCode.append(diag.highlights.last?.with(\.trailingTrivia, []).description ?? "") + } + + assertStringsEqualWithDiff( + highlightedCode.trimmingTrailingWhitespace(), + highlight.trimmingTrailingWhitespace(), + "highlight does not match", + file: spec.originatorFile, + line: spec.originatorLine + ) + } + if diag.notes.count != spec.notes.count { + XCTFail( + """ + Expected \(spec.notes.count) notes but received \(diag.notes.count): + \(diag.notes.map(\.debugDescription).joined(separator: "\n")) + """, + file: spec.originatorFile, + line: spec.originatorLine + ) + } else { + for (note, expectedNote) in zip(diag.notes, spec.notes) { + assertNote(note, in: tree, expected: expectedNote) + } + } + if diag.fixIts.count != spec.fixIts.count { + XCTFail( + """ + Expected \(spec.fixIts.count) Fix-Its but received \(diag.fixIts.count): + \(diag.fixIts.map(\.message.message).joined(separator: "\n")) + """, + file: spec.originatorFile, + line: spec.originatorLine + ) + } else { + for (fixIt, expectedFixIt) in zip(diag.fixIts, spec.fixIts) { + assertFixIt(fixIt, expected: expectedFixIt) + } + } +} + +/// Assert that expanding the given macros in the original source produces +/// the given expanded source code. +/// +/// - Parameters: +/// - originalSource: The original source code, which is expected to contain +/// macros in various places (e.g., `#stringify(x + y)`). +/// - expandedSource: The source code that we expect to see after performing +/// macro expansion on the original source. +/// - diagnostics: +/// - macros: The macros that should be expanded, provided as a dictionary +/// mapping macro names (e.g., `"stringify"`) to implementation types +/// (e.g., `StringifyMacro.self`). +/// - testModuleName: The name of the test module to use. +/// - testFileName: The name of the test file name to use. +public func assertMacroExpansion( + _ originalSource: String, + expandedSource: String, + diagnostics: [DiagnosticSpec] = [], + macros: [String: Macro.Type], + testModuleName: String = "TestModule", + testFileName: String = "test.swift", + indentationWidth: Trivia = .spaces(4), + file: StaticString = #file, + line: UInt = #line +) { + // Parse the original source file. + let origSourceFile = Parser.parse(source: originalSource) + + // Expand all macros in the source. + let context = BasicMacroExpansionContext( + sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)] + ) + let expandedSourceFile = origSourceFile.expand(macros: macros, in: context).formatted(using: BasicFormat(indentationWidth: indentationWidth)) + + assertStringsEqualWithDiff( + expandedSourceFile.description.trimmingTrailingWhitespace(), + expandedSource.trimmingTrailingWhitespace(), + file: file, + line: line + ) + + if context.diagnostics.count != diagnostics.count { + XCTFail( + """ + Expected \(diagnostics.count) diagnostics but received \(context.diagnostics.count): + \(context.diagnostics.map(\.debugDescription).joined(separator: "\n")) + """, + file: file, + line: line + ) + } else { + for (actualDiag, expectedDiag) in zip(context.diagnostics, diagnostics) { + assertDiagnostic(actualDiag, in: origSourceFile, expected: expectedDiag) + } + } +} diff --git a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift index 58a556c0917..ca727c91132 100644 --- a/Tests/SwiftBasicFormatTest/BasicFormatTests.swift +++ b/Tests/SwiftBasicFormatTest/BasicFormatTests.swift @@ -18,13 +18,30 @@ import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport +fileprivate func assertFormatted( + tree: T, + expected: String, + using format: BasicFormat = BasicFormat(indentationWidth: .spaces(4)), + file: StaticString = #file, + line: UInt = #line +) { + assertStringsEqualWithDiff(tree.formatted(using: format).description, expected, file: file, line: line) +} + fileprivate func assertFormatted( source: String, expected: String, + using format: BasicFormat = BasicFormat(indentationWidth: .spaces(4)), file: StaticString = #file, line: UInt = #line ) { - assertStringsEqualWithDiff(Parser.parse(source: source).formatted().description, expected, file: file, line: line) + assertFormatted( + tree: Parser.parse(source: source), + expected: expected, + using: format, + file: file, + line: line + ) } final class BasicFormatTest: XCTestCase { @@ -257,4 +274,57 @@ final class BasicFormatTest: XCTestCase { """ ) } + + func testDontInsertTrailingWhitespaceIfNextTokenStartsWithLeadingWhitespace() { + let tree = VariableDeclSyntax( + bindingKeyword: .keyword(.var), + bindings: PatternBindingListSyntax([ + PatternBindingSyntax( + pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier("x"))), + typeAnnotation: TypeAnnotationSyntax( + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: .identifier("Int"))) + ), + accessor: PatternBindingSyntax.Accessor( + AccessorBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space), + accessors: AccessorListSyntax([]), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + ) + ) + ]) + ) + assertFormatted( + tree: tree, + expected: """ + var x: Int { + } + """ + ) + } + + func testAccessor() { + let source = """ + struct Point { + var computed: Int { + get { 0 } + } + } + """ + + assertFormatted( + source: source, + expected: """ + struct Point { + var computed: Int { + get { + 0 + } + } + } + """, + using: BasicFormat(indentationWidth: .spaces(2)) + ) + } } diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 6a6824847a6..4a55a4a6697 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -350,19 +350,17 @@ func assertNote( _ note: Note, in tree: T, markerLocations: [String: Int], - expected spec: NoteSpec, - file: StaticString = #filePath, - line: UInt = #line + expected spec: NoteSpec ) { - XCTAssertEqual(note.message, spec.message, file: file, line: line) + XCTAssertEqual(note.message, spec.message, file: spec.file, line: spec.line) let locationConverter = SourceLocationConverter(file: "", source: tree.description) assertLocation( note.location(converter: locationConverter), in: tree, markerLocations: markerLocations, expectedLocationMarker: spec.locationMarker, - file: file, - line: line + file: spec.file, + line: spec.line ) } @@ -372,9 +370,7 @@ func assertDiagnostic( _ diag: Diagnostic, in tree: T, markerLocations: [String: Int], - expected spec: DiagnosticSpec, - file: StaticString = #filePath, - line: UInt = #line + expected spec: DiagnosticSpec ) { let locationConverter = SourceLocationConverter(file: "", source: tree.description) assertLocation( @@ -382,32 +378,32 @@ func assertDiagnostic( in: tree, markerLocations: markerLocations, expectedLocationMarker: spec.locationMarker, - file: file, - line: line + file: spec.file, + line: spec.line ) if let id = spec.id { - XCTAssertEqual(diag.diagnosticID, id, file: file, line: line) + XCTAssertEqual(diag.diagnosticID, id, file: spec.file, line: spec.line) } if let message = spec.message { - assertStringsEqualWithDiff(diag.message, message, file: file, line: line) + assertStringsEqualWithDiff(diag.message, message, file: spec.file, line: spec.line) } - XCTAssertEqual(spec.severity, diag.diagMessage.severity, file: file, line: line) + XCTAssertEqual(spec.severity, diag.diagMessage.severity, file: spec.file, line: spec.line) if diag.message.contains("\n") { XCTFail( """ Diagnostic message should only span a single line. Message was: \(diag.message) """, - file: file, - line: line + file: spec.file, + line: spec.line ) } if let highlight = spec.highlight { assertStringsEqualWithDiff( diag.highlights.map(\.description).joined().trimmingTrailingWhitespace(), highlight.trimmingTrailingWhitespace(), - file: file, - line: line + file: spec.file, + line: spec.line ) } if let notes = spec.notes { @@ -417,12 +413,12 @@ func assertDiagnostic( Expected \(notes.count) notes but received \(diag.notes.count): \(diag.notes.map(\.debugDescription).joined(separator: "\n")) """, - file: file, - line: line + file: spec.file, + line: spec.line ) } else { for (note, expectedNote) in zip(diag.notes, notes) { - assertNote(note, in: tree, markerLocations: markerLocations, expected: expectedNote, file: expectedNote.file, line: expectedNote.line) + assertNote(note, in: tree, markerLocations: markerLocations, expected: expectedNote) } } } @@ -433,15 +429,15 @@ func assertDiagnostic( Expected \(spec.fixIts.count) fix its but received \(diag.fixIts.count): \(diag.fixIts.map { $0.message.message }.joined(separator: "\n")) """, - file: file, - line: line + file: spec.file, + line: spec.line ) } else if spec.fixIts != diag.fixIts.map(\.message.message) { failStringsEqualWithDiff( diag.fixIts.map(\.message.message).joined(separator: "\n"), spec.fixIts.joined(separator: "\n"), - file: file, - line: line + file: spec.file, + line: spec.line ) } } @@ -603,7 +599,7 @@ func assertParse( ) } else { for (diag, expectedDiag) in zip(diags, expectedDiagnostics) { - assertDiagnostic(diag, in: tree, markerLocations: markerLocations, expected: expectedDiag, file: expectedDiag.file, line: expectedDiag.line) + assertDiagnostic(diag, in: tree, markerLocations: markerLocations, expected: expectedDiag) } } diff --git a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift index 1979898d1ba..7ab790b3eaa 100644 --- a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift +++ b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift @@ -10,12 +10,13 @@ // //===----------------------------------------------------------------------===// +import _SwiftSyntaxTestSupport import SwiftDiagnostics import SwiftParser import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -import _SwiftSyntaxTestSupport +import SwiftSyntaxMacrosTestSupport import XCTest enum CustomError: Error, CustomStringConvertible { @@ -650,55 +651,6 @@ public struct UnwrapMacro: CodeItemMacro { } } -// MARK: Assertion helper functions - -/// Assert that expanding the given macros in the original source produces -/// the given expanded source code. -/// -/// - Parameters: -/// - macros: The macros that should be expanded, provided as a dictionary -/// mapping macro names (e.g., `"stringify"`) to implementation types -/// (e.g., `StringifyMacro.self`). -/// - testModuleName: The name of the test module to use. -/// - testFileName: The name of the test file name to use. -/// - originalSource: The original source code, which is expected to contain -/// macros in various places (e.g., `#stringify(x + y)`). -/// - expandedSource: The source code that we expect to see after performing -/// macro expansion on the original source. -public func assertMacroExpansion( - macros: [String: Macro.Type], - testModuleName: String = "TestModule", - testFileName: String = "test.swift", - _ originalSource: String, - _ expandedSource: String, - diagnosticStrings: [String] = [], - file: StaticString = #file, - line: UInt = #line -) { - // Parse the original source file. - let origSourceFile = Parser.parse(source: originalSource) - - // Expand all macros in the source. - let context = BasicMacroExpansionContext( - sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)] - ) - let expandedSourceFile = origSourceFile.expand(macros: macros, in: context) - - assertStringsEqualWithDiff( - expandedSourceFile.description, - expandedSource, - file: file, - line: line - ) - - let diags = context.diagnostics - XCTAssertEqual(diags.count, diagnosticStrings.count) - for (actualDiag, expectedDiag) in zip(diags, diagnosticStrings) { - let actualMessage = actualDiag.message - XCTAssertEqual(actualMessage, expectedDiag) - } -} - // MARK: Tests /// The set of test macros we use here. @@ -721,51 +673,56 @@ public let testMacros: [String: Macro.Type] = [ ] final class MacroSystemTests: XCTestCase { + private let indentationWidth: Trivia = .spaces(2) + func testExpressionExpansion() { assertMacroExpansion( - macros: testMacros, """ let b = #stringify(x + y) #colorLiteral(red: 0.5, green: 0.5, blue: 0.25, alpha: 1.0) """, - """ - let b = (x + y, "x + y") - .init(_colorLiteralRed: 0.5, green: 0.5, blue: 0.25, alpha: 1.0) - """ + expandedSource: """ + let b = (x + y, "x + y") + .init(_colorLiteralRed: 0.5, green: 0.5, blue: 0.25, alpha: 1.0) + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testStringifyExpression() { assertMacroExpansion( - macros: ["stringify": StringifyMacro.self], """ _ = #stringify({ () -> Bool in print("hello") return true }) """, - """ - _ = ({ () -> Bool in - print("hello") - return true - }, #"{ () -> Bool in\\#n print("hello")\\#n return true\\#n}"#) - """ + expandedSource: """ + _ = ({ () -> Bool in + print("hello") + return true + }, #"{ () -> Bool in\\#n print("hello")\\#n return true\\#n}"#) + """, + macros: ["stringify": StringifyMacro.self], + indentationWidth: indentationWidth ) } func testLocationExpansions() { assertMacroExpansion( - macros: testMacros, - testModuleName: "MyModule", - testFileName: "taylor.swift", """ let b = #fileID let c = #column """, - """ - let b = "MyModule/taylor.swift" - let c = 9 - """ + expandedSource: """ + let b = "MyModule/taylor.swift" + let c = 9 + """, + macros: testMacros, + testModuleName: "MyModule", + testFileName: "taylor.swift", + indentationWidth: indentationWidth ) } @@ -781,19 +738,19 @@ final class MacroSystemTests: XCTestCase { func testContextIndependence() { assertMacroExpansion( - macros: ["checkContext": CheckContextIndependenceMacro.self], """ let b = #checkContext """, - """ - let b = () - """ + expandedSource: """ + let b = () + """, + macros: ["checkContext": CheckContextIndependenceMacro.self], + indentationWidth: indentationWidth ) } func testErrorExpansion() { assertMacroExpansion( - macros: testMacros, """ #myError("please don't do that") struct X { @@ -804,102 +761,113 @@ final class MacroSystemTests: XCTestCase { } } """, - """ + expandedSource: """ - struct X { - func f() { } - func g() { + struct X { + func f() { + } + func g() { + } } - } - """, - diagnosticStrings: [ - "please don't do that", - "#error macro requires a string literal", - "worse", - ] + """, + diagnostics: [ + DiagnosticSpec(message: "please don't do that", line: 1, column: 1, highlight: #"#myError("please don't do that")"#), + DiagnosticSpec(message: "#error macro requires a string literal", line: 4, column: 3, highlight: #"#myError(bad)"#), + DiagnosticSpec(message: "worse", line: 6, column: 5, highlight: #"#myError("worse")"#), + ], + macros: testMacros, + indentationWidth: indentationWidth ) } func testBitwidthNumberedStructsExpansion() { assertMacroExpansion( - macros: testMacros, """ #bitwidthNumberedStructs("MyInt") """, - """ + expandedSource: """ - struct MyInt8 { } - struct MyInt16 { } - struct MyInt32 { } - struct MyInt64 { } - """ + struct MyInt8 { + } + struct MyInt16 { + } + struct MyInt32 { + } + struct MyInt64 { + } + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testPropertyWrapper() { assertMacroExpansion( - macros: testMacros, """ @wrapProperty("MyWrapperType") var x: Int """, - """ + expandedSource: """ - var x: Int { - get { - _x.wrappedValue - } - set { - _x.wrappedValue = newValue + var x: Int { + get { + _x.wrappedValue + } + set { + _x.wrappedValue = newValue + } } - } - private var _x: MyWrapperType - """ + private var _x: MyWrapperType + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testAddCompletionHandler() { assertMacroExpansion( - macros: testMacros, """ @addCompletionHandler func f(a: Int, for b: String, _ value: Double) async -> String { } """, - """ + expandedSource: """ - func f(a: Int, for b: String, _ value: Double) async -> String { } + func f(a: Int, for b: String, _ value: Double) async -> String { + } - func f(a: Int, for b: String, _ value: Double, completionHandler: (String) -> Void) { - Task { - completionHandler(await f(a: a, for: b, value)) + func f(a: Int, for b: String, _ value: Double, completionHandler: (String) -> Void) { + Task { + completionHandler(await f(a: a, for: b, value)) + } } - } - """ + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testAddBackingStorage() { assertMacroExpansion( - macros: testMacros, """ @addBackingStorage struct S { var value: Int } """, - """ + expandedSource: """ - struct S { - var value: Int - var _storage: Storage - } - """ + struct S { + var value: Int + var _storage: Storage + } + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testWrapAllProperties() { assertMacroExpansion( - macros: testMacros, """ @wrapAllProperties struct Point { @@ -914,28 +882,35 @@ final class MacroSystemTests: XCTestCase { func test() {} } """, - """ + expandedSource: """ + + struct Point { + @Wrapper + var x: Int + @Wrapper + var y: Int + @Wrapper + var description: String { + "" + } + @Wrapper + var computed: Int { + get { + 0 + } + set { + } + } - struct Point { - @Wrapper - var x: Int - @Wrapper - var y: Int - @Wrapper - var description: String { "" } - @Wrapper - var computed: Int { - get { 0 } - set {} + func test() { + } } - - func test() {} - } - """ + """, + macros: testMacros, + indentationWidth: indentationWidth ) assertMacroExpansion( - macros: testMacros, """ @wrapStoredProperties struct Point { @@ -952,30 +927,37 @@ final class MacroSystemTests: XCTestCase { func test() {} } """, - """ + expandedSource: """ - struct Point { - @Wrapper - var x: Int - @Wrapper - var y: Int + struct Point { + @Wrapper + var x: Int + @Wrapper + var y: Int - var description: String { "" } + var description: String { + "" + } - var computed: Int { - get { 0 } - set {} - } + var computed: Int { + get { + 0 + } + set { + } + } - func test() {} - } - """ + func test() { + } + } + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testTypeWrapperTransform() { assertMacroExpansion( - macros: testMacros, """ @customTypeWrapper struct Point { @@ -983,36 +965,36 @@ final class MacroSystemTests: XCTestCase { var y: Int } """, - // FIXME: Accessor brace indentation is off - """ - - struct Point { - var x: Int { - get { - _storage[wrappedKeyPath: \\.x] + expandedSource: """ + + struct Point { + var x: Int { + get { + _storage[wrappedKeyPath: \\.x] + } + set { + _storage[wrappedKeyPath: \\.x] = newValue + } } - set { - _storage[wrappedKeyPath: \\.x] = newValue + var y: Int { + get { + _storage[wrappedKeyPath: \\.y] + } + set { + _storage[wrappedKeyPath: \\.y] = newValue + } } - } - var y: Int { - get { - _storage[wrappedKeyPath: \\.y] - } - set { - _storage[wrappedKeyPath: \\.y] = newValue - } - } - var _storage: Wrapper - } - """ + var _storage: Wrapper + } + """, + macros: testMacros, + indentationWidth: indentationWidth ) } func testUnwrap() { assertMacroExpansion( - macros: testMacros, #""" let x: Int? = 1 let y: Int? = nil @@ -1022,23 +1004,37 @@ final class MacroSystemTests: XCTestCase { fatalError("nil is \\($0)") } """#, - #""" - let x: Int? = 1 - let y: Int? = nil - let z: Int? = 3 - guard let x else { fatalError("'x' is nil") } - guard let y else { fatalError("'y' is nil") } - guard let z else { fatalError("'z' is nil") } - guard let x else { { - fatalError("nil is \\($0)") - }("x") } - guard let y else { { - fatalError("nil is \\($0)") - }("y") } - guard let z else { { - fatalError("nil is \\($0)") - }("z") } - """# + expandedSource: #""" + let x: Int? = 1 + let y: Int? = nil + let z: Int? = 3 + guard let x else { + fatalError("'x' is nil") + } + guard let y else { + fatalError("'y' is nil") + } + guard let z else { + fatalError("'z' is nil") + } + guard let x else { + { + fatalError("nil is \\($0)") + }("x") + } + guard let y else { + { + fatalError("nil is \\($0)") + }("y") + } + guard let z else { + { + fatalError("nil is \\($0)") + }("z") + } + """#, + macros: testMacros, + indentationWidth: indentationWidth ) } }