|
1 | 1 | import XCTest
|
2 | 2 | @_spi(RawSyntax) import SwiftSyntax
|
3 | 3 | @_spi(Testing) @_spi(RawSyntax) import SwiftParser
|
| 4 | +import SwiftDiagnostics |
| 5 | +import _SwiftSyntaxTestSupport |
4 | 6 |
|
5 | 7 | // MARK: Lexing Assertions
|
6 | 8 |
|
@@ -49,33 +51,147 @@ func AssertEqualTokens(_ actual: [Lexer.Lexeme], _ expected: [Lexer.Lexeme], fil
|
49 | 51 |
|
50 | 52 | // MARK: Parsing Assertions
|
51 | 53 |
|
| 54 | +/// An abstract data structure to describe how a diagnostic produced by the parser should look like. |
| 55 | +struct DiagnosticSpec { |
| 56 | + /// The name of a maker (of the form `#^DIAG^#`) in the source code that marks the location where the diagnostis should be produced. |
| 57 | + let locationMarker: String |
| 58 | + /// If not `nil`, assert that the diagnostic has the given ID. |
| 59 | + let id: MessageID? |
| 60 | + /// If not `nil`, assert that the diagnostic has the given message. |
| 61 | + let message: String? |
| 62 | + /// If not `nil`, assert that the highlighted range has this content. |
| 63 | + let highlight: String? |
| 64 | + /// If not `nil`, assert that the diagnostic contains fix-its with these messages. |
| 65 | + /// Use the `fixedSource` parameter on `AssertParse` to check that applying the Fix-It yields the expected result. |
| 66 | + let fixIts: [String]? |
| 67 | + |
| 68 | + init(locationMarker: String = "DIAG", id: MessageID? = nil, message: String?, highlight: String? = nil, fixIts: [String]? = nil) { |
| 69 | + self.locationMarker = locationMarker |
| 70 | + self.id = id |
| 71 | + self.message = message |
| 72 | + self.highlight = highlight |
| 73 | + self.fixIts = fixIts |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +class FixItApplier: SyntaxRewriter { |
| 78 | + var changes: [FixIt.Change] |
| 79 | + |
| 80 | + init(diagnostics: [Diagnostic]) { |
| 81 | + self.changes = diagnostics.flatMap({ $0.fixIts }).flatMap({ $0.changes }) |
| 82 | + } |
| 83 | + |
| 84 | + public override func visitAny(_ node: Syntax) -> Syntax? { |
| 85 | + for change in changes { |
| 86 | + switch change { |
| 87 | + case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id: |
| 88 | + return newNode |
| 89 | + default: |
| 90 | + break |
| 91 | + } |
| 92 | + } |
| 93 | + return nil |
| 94 | + } |
| 95 | + |
| 96 | + /// Applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree. |
| 97 | + public static func applyFixes<T: SyntaxProtocol>(in diagnostics: [Diagnostic], to tree: T) -> Syntax { |
| 98 | + let applier = FixItApplier(diagnostics: diagnostics) |
| 99 | + return applier.visit(Syntax(tree)) |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +/// Assert that the diagnostic `diag` produced in `tree` matches `spec`, |
| 104 | +/// using `markerLocations` to translate markers to actual source locations. |
| 105 | +func AssertDiagnostic<T: SyntaxProtocol>( |
| 106 | + _ diag: Diagnostic, |
| 107 | + in tree: T, |
| 108 | + markerLocations: [String: Int], |
| 109 | + expected spec: DiagnosticSpec, |
| 110 | + file: StaticString = #filePath, |
| 111 | + line: UInt = #line |
| 112 | +) { |
| 113 | + if let markerLoc = markerLocations[spec.locationMarker] { |
| 114 | + let locationConverter = SourceLocationConverter(file: "/test.swift", source: tree.description) |
| 115 | + let actualLocation = diag.location(converter: locationConverter) |
| 116 | + let expectedLocation = locationConverter.location(for: AbsolutePosition(utf8Offset: markerLoc)) |
| 117 | + if let actualLine = actualLocation.line, |
| 118 | + let actualColumn = actualLocation.column, |
| 119 | + let expectedLine = expectedLocation.line, |
| 120 | + let expectedColumn = expectedLocation.column { |
| 121 | + XCTAssertEqual( |
| 122 | + actualLine, expectedLine, |
| 123 | + "Expected diagnostic on line \(expectedLine) but got \(actualLine)", |
| 124 | + file: file, line: line |
| 125 | + ) |
| 126 | + XCTAssertEqual( |
| 127 | + actualColumn, expectedColumn, |
| 128 | + "Expected diagnostic on column \(expectedColumn) but got \(actualColumn)", |
| 129 | + file: file, line: line |
| 130 | + ) |
| 131 | + } else { |
| 132 | + XCTFail("Failed to resolve diagnostic location to line/column", file: file, line: line) |
| 133 | + } |
| 134 | + } else { |
| 135 | + XCTFail("Did not find marker #^\(spec.locationMarker)^# in the source code", file: file, line: line) |
| 136 | + } |
| 137 | + if let id = spec.id { |
| 138 | + XCTAssertEqual(diag.diagnosticID, id, file: file, line: line) |
| 139 | + } |
| 140 | + if let message = spec.message { |
| 141 | + XCTAssertEqual(diag.message, message, file: file, line: line) |
| 142 | + } |
| 143 | + if let highlight = spec.highlight { |
| 144 | + AssertStringsEqualWithDiff(diag.highlights.map(\.description).joined(), highlight, file: file, line: line) |
| 145 | + } |
| 146 | + if let fixIts = spec.fixIts { |
| 147 | + XCTAssertEqual( |
| 148 | + fixIts, diag.fixIts.map(\.message.message), |
| 149 | + "Fix-Its for diagnostic did not match expected Fix-Its", |
| 150 | + file: file, line: line |
| 151 | + ) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +/// Parse `markedSource` and perform the following assertions: |
| 156 | +/// - The parsed syntax tree should be printable back to the original source code (round-tripping) |
| 157 | +/// - Parsing produced the given `diagnostics` (`diagnostics = []` asserts that the parse was successful) |
| 158 | +/// - If `fixedSource` is not `nil`, assert that applying all fixes from the diagnostics produces `fixedSource` |
| 159 | +/// The source file can be marked with markers of the form `#^DIAG^#` to mark source locations that can be referred to by `diagnostics`. |
| 160 | +/// These markers are removed before parsing the source file. |
| 161 | +/// By default, `DiagnosticSpec` asserts that the diagnostics is produced at a location marked by `#^DIAG^#`. |
| 162 | +/// `parseSyntax` can be used to adjust the production that should be used as the entry point to parse the source code. |
52 | 163 | func AssertParse<Node: RawSyntaxNodeProtocol>(
|
53 |
| - _ parseSyntax: (inout Parser) -> Node, |
54 |
| - allowErrors: Bool = false, |
| 164 | + _ markedSource: String, |
| 165 | + _ parseSyntax: (inout Parser) -> Node = { $0.parseSourceFile() }, |
| 166 | + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], |
| 167 | + fixedSource expectedFixedSource: String? = nil, |
55 | 168 | file: StaticString = #file,
|
56 |
| - line: UInt = #line, |
57 |
| - _ source: () -> String |
58 |
| -) throws { |
| 169 | + line: UInt = #line |
| 170 | +) { |
59 | 171 | // Verify the parser can round-trip the source
|
60 |
| - let src = source() |
61 |
| - var source = src |
62 |
| - source.withUTF8 { buf in |
| 172 | + let (markerLocations, source) = extractMarkers(markedSource) |
| 173 | + var src = source |
| 174 | + src.withUTF8 { buf in |
63 | 175 | var parser = Parser(buf)
|
64 | 176 | withExtendedLifetime(parser) {
|
65 |
| - let parse = Syntax(raw: parseSyntax(&parser).raw) |
66 |
| - AssertStringsEqualWithDiff("\(parse)", src, additionalInfo: """ |
| 177 | + let tree = Syntax(raw: parseSyntax(&parser).raw) |
| 178 | + AssertStringsEqualWithDiff("\(tree)", source, additionalInfo: """ |
| 179 | + Source failed to round-trip. |
| 180 | +
|
67 | 181 | Actual syntax tree:
|
68 |
| - \(parse.recursiveDescription) |
| 182 | + \(tree.recursiveDescription) |
| 183 | + """, file: file, line: line) |
| 184 | + let diags = ParseDiagnosticsGenerator.diagnostics(for: tree) |
| 185 | + XCTAssertEqual(diags.count, expectedDiagnostics.count, """ |
| 186 | + Expected \(expectedDiagnostics.count) diagnostics but received \(diags.count): |
| 187 | + \(diags.map(\.debugDescription).joined(separator: "\n")) |
69 | 188 | """, file: file, line: line)
|
70 |
| - if !allowErrors { |
71 |
| - let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: Syntax(raw: parse.raw)) |
72 |
| - XCTAssertEqual( |
73 |
| - diagnostics.count, 0, |
74 |
| - """ |
75 |
| - Received the following diagnostics while parsing the source code: |
76 |
| - \(diagnostics) |
77 |
| - """, |
78 |
| - file: file, line: line) |
| 189 | + for (diag, expectedDiag) in zip(diags, expectedDiagnostics) { |
| 190 | + AssertDiagnostic(diag, in: tree, markerLocations: markerLocations, expected: expectedDiag, file: file, line: line) |
| 191 | + } |
| 192 | + if let expectedFixedSource = expectedFixedSource { |
| 193 | + let fixedSource = FixItApplier.applyFixes(in: diags, to: tree).description |
| 194 | + AssertStringsEqualWithDiff(fixedSource, expectedFixedSource, file: file, line: line) |
79 | 195 | }
|
80 | 196 | }
|
81 | 197 | }
|
|
0 commit comments