Skip to content

Commit bb62ba2

Browse files
authored
Merge pull request swiftlang#641 from ahoppen/ahoppen/test-improvements
Unify all parser tests to use an enhanced version `AssertParse`
2 parents e8f596c + 459da60 commit bb62ba2

16 files changed

+1080
-1122
lines changed

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public enum StaticParserError: String, DiagnosticMessage {
9090
}
9191

9292
public enum StaticParserFixIt: String, FixItMessage {
93-
case moveThrowBeforeArrow = "Move 'throws' in before of '->'"
93+
case moveThrowBeforeArrow = "Move 'throws' before '->'"
9494

9595
public var message: String { self.rawValue }
9696

Sources/SwiftParser/SwiftParser.docc/FixingBugs.md

+20-29
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ Once you’ve written a test case (see below), set a breakpoint in `Parser.parse
1010

1111
1. Add a new test case in `SwiftParserTest` that looks like the following
1212
```swift
13-
try AssertParse({ $0.parseSourceFile() }) {
13+
AssertParse(
1414
"""
1515
<#your code that does not round trip#>
1616
"""
17-
}
17+
)
1818
```
1919
2. Run the test case, read the error message to figure out which part of the source file does not round-trip
20-
3. Optional: Reduce the test case even further by deleting more source code and calling into a specific production of the parser instead of `Parser.parseSourceFile`
2120

2221

2322
## Parse of Valid Source Failed
@@ -28,20 +27,14 @@ Diagnostics are produced when the parsed syntax tree contains missing or unexpec
2827

2928
1. Add a test case in `SwiftParserTest` that looks like the following
3029
```swift
31-
let source = """
32-
<#your code that produces an invalid syntax tree#>
33-
"""
34-
35-
let tree = withParser(source: source) {
36-
Syntax(raw: $0.parseSourceFile().raw)
37-
}
38-
XCTAssertHasSubstructure(
39-
tree,
40-
<#create a syntax node that you expect the tree to have#>
30+
AssertParse(
31+
"""
32+
<#your code that produces an invalid syntax tree#>
33+
""",
34+
substructure: <#create a syntax node that you expect the tree to have#>
4135
)
4236
```
43-
2. Optional: Reduce the test case even further by deleting more source code and calling into a specific production of the parser instead of `Parser.parseSourceFile`
44-
3. Run the test case and navigate the debugger to the place that produced the invalid syntax node.
37+
2. Run the test case and navigate the debugger to the place that produced the invalid syntax node.
4538

4639
## Unhelpful Diagnostic Produced
4740

@@ -51,31 +44,29 @@ Unhelpful diagnostics can result from two reasons:
5144

5245
To distinguish these cases run the following command and look at the dumped syntax tree. Use your own judgment to decide whether this models the intended meaning of the source code reasonably well.
5346
```
54-
swift-parser-test print-tree /path/to/file/with/bad/diagnostic
47+
swift-parser-test print-tree /path/to/file/with/unhelpful/diagnostic.swift
5548
```
5649
5750
Fixing the first case where the parser does not recover according to the user’s intent is similar to [Parse of Valid Source Code Produced an Invalid Syntax Tree](#Parse-of-Valid-Source-Code-Produced-an-Invalid-Syntax-Tree). See <doc:SwiftParser/ParserRecovery> for documentation how parser recovery works and determine how to recover better from the invalid source code.
5851
5952
To add a new, more contextual diagnostic, perform the following steps.
6053
61-
1. Add a test case to `DiagnosticTests.swift` like the following:
54+
1. Add a test case in `SwiftParserTest` that looks like the following
6255
6356
```swift
64-
let source = """
65-
<#your code that produces a bad diagnostic#>
66-
}
67-
"""
68-
let loop = withParser(source: source) {
69-
Syntax(raw: $0.parserSourceFile().raw)
70-
}
57+
AssertParse(
58+
"""
59+
<#your code that produced the unhelpful diagnostic#>
60+
""",
61+
diagnostics: [
62+
DiagnosticSpec(message: "<#expected diagnostic message#>")
63+
]
64+
)
7165
```
72-
2. Optional: Call a more specific production than `parseSourceFile` in the test case.
66+
2. Mark the location at which you expect the diagnostic to be produced with `#^DIAG^#`. If you expect multiple diagnostics to be produced, you can use multiple of these markers with different names and use these markers by passing a `locationMarker` to `DiagnosticSpec`.
7367
3. Determine which node encompasses all information that is necessary to produce the improved diagnostic – for example `FunctionSignatureSyntax` contains all information to diagnose if the `throws` keyword was written after the `->` instead of in front of it.
7468
4. If the diagnostic message you want to emit does not exist yet, add a case to <doc:SwiftParser/DiagnosticKind> for the new diagnostic.
7569
5. If the function does not already exist, write a new visit method on <doc:SwiftParser/ParseDiagnosticsGenerator>.
7670
6. In that visitation method, detect the pattern for which the improved diagnostic should be emitted and emit it using `diagnostics.append`.
7771
7. Mark the missing or garbage nodes that are covered by the new diagnostic as handled by adding their `SyntaxIdentifier`s to `handledNodes`.
78-
8. Assert that the new diagnostic is emitted by addding the following to your test case:
79-
```swift
80-
XCTAssertSingleDiagnostic(in: tree, line: <#expected line#>, column: <#expected column#>, expectedKind: .<#expected diagnostic kind#>)
81-
```
72+
8. If the diagnostic produces Fix-Its assert that they are generated by adding the Fix-It's message to the `DiagnosticSpec` with the `fixIt` parameter and asserting that applying the Fix-Its produces the correct source code by adding the `fixedSource` parameter to `AssertParse`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//===--- LocationMarkers.swift --------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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+
/// Finds all marked ranges in the given text, see `Marker`.
14+
fileprivate func findMarkedRanges(text: String) -> [Marker] {
15+
var markers = [Marker]()
16+
while let marker = nextMarkedRange(text: text, from: markers.last?.range.upperBound ?? text.startIndex) {
17+
markers.append(marker)
18+
}
19+
return markers
20+
}
21+
22+
fileprivate func nextMarkedRange(text: String, from: String.Index) -> Marker? {
23+
guard let start = text.range(of: "#^", range: from ..< text.endIndex),
24+
let end = text.range(of: "^#", range: start.upperBound ..< text.endIndex) else {
25+
return nil
26+
}
27+
28+
let markerRange = start.lowerBound ..< end.upperBound
29+
let name = text[start.upperBound ..< end.lowerBound]
30+
31+
// Expand to the whole line if the line only contains the marker
32+
let lineRange = text.lineRange(for: start)
33+
if text[lineRange].trimmingCharacters(in: .whitespacesAndNewlines) == text[markerRange] {
34+
return Marker(name: name, range: lineRange)
35+
}
36+
return Marker(name: name, range: markerRange)
37+
}
38+
39+
fileprivate struct Marker {
40+
/// The name of the marker without the `#^` and `^#` markup.
41+
let name: Substring
42+
/// The range of the marker.
43+
///
44+
/// If the marker contains all the the non-whitepace characters on the line,
45+
/// this is the range of the entire line. Otherwise it's the range of the
46+
/// marker itself, including the `#^` and `^#` markup.
47+
let range: Range<String.Index>
48+
}
49+
50+
public func extractMarkers(_ markedText: String) -> (markers: [String: Int], textWithoutMarkers: String) {
51+
var text = ""
52+
var markers = [String: Int]()
53+
var lastIndex = markedText.startIndex
54+
for marker in findMarkedRanges(text: markedText) {
55+
text += markedText[lastIndex ..< marker.range.lowerBound]
56+
lastIndex = marker.range.upperBound
57+
58+
assert(markers[String(marker.name)] == nil, "Marker names must be unique")
59+
markers[String(marker.name)] = text.utf8.count
60+
}
61+
text += markedText[lastIndex ..< markedText.endIndex]
62+
63+
return (markers, text)
64+
}

Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift

+5-106
Original file line numberDiff line numberDiff line change
@@ -33,60 +33,6 @@ public func XCTAssertNextIsNil<Iterator: IteratorProtocol>(_ iterator: inout Ite
3333
XCTAssertNil(iterator.next())
3434
}
3535

36-
/// Verifies that the tree parsed from `actual` has the same structure as
37-
/// `expected` when parsed with `parse`, ie. it has the same structure and
38-
/// optionally the same trivia (if `includeTrivia` is set).
39-
public func XCTAssertSameStructure(
40-
_ actual: String,
41-
parse: (String) throws -> Syntax,
42-
_ expected: Syntax,
43-
includeTrivia: Bool = false,
44-
file: StaticString = #filePath, line: UInt = #line
45-
) throws {
46-
let actualTree = try parse(actual)
47-
XCTAssertSameStructure(actualTree, expected, includeTrivia: includeTrivia, file: file, line: line)
48-
}
49-
50-
/// Verifies that two trees are equivalent, ie. they have the same structure
51-
/// and optionally the same trivia if `includeTrivia` is set.
52-
public func XCTAssertSameStructure<ActualTree, ExpectedTree>(
53-
_ actual: ActualTree,
54-
_ expected: ExpectedTree,
55-
includeTrivia: Bool = false,
56-
file: StaticString = #filePath, line: UInt = #line
57-
)
58-
where ActualTree: SyntaxProtocol, ExpectedTree: SyntaxProtocol
59-
{
60-
let diff = actual.findFirstDifference(baseline: expected, includeTrivia: includeTrivia)
61-
XCTAssertNil(diff, diff!.debugDescription, file: file, line: line)
62-
}
63-
64-
/// See `SubtreeMatcher.assertSameStructure`.
65-
public func XCTAssertHasSubstructure<ExpectedTree: SyntaxProtocol>(
66-
_ markedText: String,
67-
parse: (String) throws -> Syntax,
68-
afterMarker: String? = nil,
69-
_ expected: ExpectedTree,
70-
includeTrivia: Bool = false,
71-
file: StaticString = #filePath, line: UInt = #line
72-
) throws {
73-
let subtreeMatcher = try SubtreeMatcher(markedText, parse: parse)
74-
try subtreeMatcher.assertSameStructure(afterMarker: afterMarker, Syntax(expected), file: file, line: line)
75-
}
76-
77-
/// See `SubtreeMatcher.assertSameStructure`.
78-
public func XCTAssertHasSubstructure<ActualTree, ExpectedTree>(
79-
_ actualTree: ActualTree,
80-
_ expected: ExpectedTree,
81-
includeTrivia: Bool = false,
82-
file: StaticString = #filePath, line: UInt = #line
83-
) throws
84-
where ActualTree: SyntaxProtocol, ExpectedTree: SyntaxProtocol
85-
{
86-
let subtreeMatcher = SubtreeMatcher(Syntax(actualTree))
87-
try subtreeMatcher.assertSameStructure(Syntax(expected), file: file, line: line)
88-
}
89-
9036
/// Allows matching a subtrees of the given `markedText` against
9137
/// `baseline`/`expected` trees, where a combination of markers and the type
9238
/// of the `expected` tree is used to first find the subtree to match. Note
@@ -135,24 +81,14 @@ public struct SubtreeMatcher {
13581
private var actualTree: Syntax
13682

13783
public init(_ markedText: String, parse: (String) throws -> Syntax) throws {
138-
var text = ""
139-
var markers = [String: Int]()
140-
var lastIndex = markedText.startIndex
141-
for marker in findMarkedRanges(text: markedText) {
142-
text += markedText[lastIndex ..< marker.range.lowerBound]
143-
lastIndex = marker.range.upperBound
144-
145-
assert(markers[String(marker.name)] == nil, "Marker names must be unique")
146-
markers[String(marker.name)] = text.utf8.count
147-
}
148-
text += markedText[lastIndex ..< markedText.endIndex]
84+
let (markers, text) = extractMarkers(markedText)
14985

15086
self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers
15187
self.actualTree = try parse(text)
15288
}
15389

154-
public init(_ actualTree: Syntax) {
155-
self.markers = ["DEFAULT": 0]
90+
public init(_ actualTree: Syntax, markers: [String: Int]) {
91+
self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers
15692
self.actualTree = actualTree
15793
}
15894

@@ -172,8 +108,8 @@ public struct SubtreeMatcher {
172108
return subtree.findFirstDifference(baseline: baseline, includeTrivia: includeTrivia)
173109
}
174110

175-
/// Same as `XCTAssertSameStructure`, but uses the subtree found from parsing
176-
/// the text passed into `init(markedText:)` as the `actual` tree.
111+
/// Verifies that the the subtree found from parsing the text passed into
112+
/// `init(markedText:)` has the same structure as `expected`.
177113
public func assertSameStructure(afterMarker: String? = nil, _ expected: Syntax, includeTrivia: Bool = false,
178114
file: StaticString = #filePath, line: UInt = #line) throws {
179115
let diff = try findFirstDifference(afterMarker: afterMarker, baseline: expected, includeTrivia: includeTrivia)
@@ -195,43 +131,6 @@ public enum SubtreeError: Error, CustomStringConvertible {
195131
}
196132
}
197133

198-
/// Finds all marked ranges in the given text, see `Marker`.
199-
fileprivate func findMarkedRanges(text: String) -> [Marker] {
200-
var markers = [Marker]()
201-
while let marker = nextMarkedRange(text: text, from: markers.last?.range.upperBound ?? text.startIndex) {
202-
markers.append(marker)
203-
}
204-
return markers
205-
}
206-
207-
fileprivate func nextMarkedRange(text: String, from: String.Index) -> Marker? {
208-
guard let start = text.range(of: "#^", range: from ..< text.endIndex),
209-
let end = text.range(of: "^#", range: start.upperBound ..< text.endIndex) else {
210-
return nil
211-
}
212-
213-
let markerRange = start.lowerBound ..< end.upperBound
214-
let name = text[start.upperBound ..< end.lowerBound]
215-
216-
// Expand to the whole line if the line only contains the marker
217-
let lineRange = text.lineRange(for: start)
218-
if text[lineRange].trimmingCharacters(in: .whitespacesAndNewlines) == text[markerRange] {
219-
return Marker(name: name, range: lineRange)
220-
}
221-
return Marker(name: name, range: markerRange)
222-
}
223-
224-
fileprivate struct Marker {
225-
/// The name of the marker without the `#^` and `^#` markup.
226-
let name: Substring
227-
/// The range of the marker.
228-
///
229-
/// If the marker contains all the the non-whitepace characters on the line,
230-
/// this is the range of the entire line. Otherwise it's the range of the
231-
/// marker itself, including the `#^` and `^#` markup.
232-
let range: Range<String.Index>
233-
}
234-
235134
fileprivate class SyntaxTypeFinder: SyntaxAnyVisitor {
236135
private let offset: Int
237136
private let type: SyntaxProtocol.Type

0 commit comments

Comments
 (0)