-
Notifications
You must be signed in to change notification settings - Fork 441
/
Copy pathSyntax+Assertions.swift
207 lines (186 loc) · 6.76 KB
/
Syntax+Assertions.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Syntax assertion helpers.
//
//===----------------------------------------------------------------------===//
#if swift(>=6)
public import SwiftSyntax
private import XCTest
#else
import SwiftSyntax
import XCTest
#endif
/// Verifies that there is a next item returned by the iterator and that it
/// satisfies the given predicate.
public func XCTAssertNext<Iterator: IteratorProtocol>(
_ iterator: inout Iterator,
satisfies predicate: (Iterator.Element) throws -> Bool,
file: StaticString = #filePath,
line: UInt = #line
) throws {
let next = try XCTUnwrap(iterator.next(), file: file, line: line)
XCTAssertTrue(try predicate(next), file: file, line: line)
}
/// Verifies that the iterator is exhausted.
public func XCTAssertNextIsNil<Iterator: IteratorProtocol>(_ iterator: inout Iterator) {
XCTAssertNil(iterator.next())
}
/// Allows matching a subtrees of the given `markedText` against
/// `baseline`/`expected` trees, where a combination of markers and the type
/// of the `expected` tree is used to first find the subtree to match. Note
/// any markers are removed before the source is parsed.
///
/// As an example, given some source with markers to parse:
/// ```
/// let subtreeMatcher = SubtreeMatcher("""
/// func foo(1️⃣x y: Double,
/// 2️⃣first second third: Int) {}
/// """
/// ```
///
/// And then some asserts:
/// ```
/// let firstExpectedParam = ... // make the first expected parameter
/// subtreeMatcher.assertSameStructure(from: "1️⃣", firstExpectedParam)
///
/// let secondExpectedParam = ... // make the second expected parameter
/// subtreeMatcher.assertSameStructure(from: "2️⃣", secondExpectedParam)
/// ```
///
/// Before parsing, `1️⃣` and `2️⃣` will first be removed from the
/// source test.
///
/// The subtree matched in `1️⃣` will start at the first node after
/// `1️⃣` that matches the root type of `firstExpectedParam`.
///
/// There's no *need* to specify a marker, eg. if we want to match on the first
/// parameter only we could instead write:
/// ```
/// let subtreeMatcher = SubtreeMatcher("""
/// func foo(first second third: Int) {}
/// """
/// let expectedParam = ... // make the first expected parameter
/// subtreeMatcher.assertSameStructure(expectedParam)
/// ```
///
/// Since no marker was given the matched subtree will just start at the first
/// node matching `expectedParam`'s type.
public struct SubtreeMatcher {
/// Maps marker names to the UTF-8 byte offset for the character at which
/// they occurred in the source *with markers removed*.
private let markers: [String: Int]
/// The syntax tree from parsing source *with markers removed*.
private var actualTree: Syntax
public init(_ markedText: String, parse: (String) throws -> Syntax) rethrows {
let (markers, text) = extractMarkers(markedText)
self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers
self.actualTree = try parse(text)
}
public init(_ actualTree: some SyntaxProtocol, markers: [String: Int]) {
self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers
self.actualTree = Syntax(actualTree)
}
/// Same as `Syntax.findFirstDifference(baseline:includeTrivia:)`, but
/// matches against the first subtree from parsing `markedText` that is after
/// `afterMarker` with the root matching the root type of `baseline`.
public func findFirstDifference(
afterMarker: String? = nil,
baseline: some SyntaxProtocol,
includeTrivia: Bool = false
) throws -> TreeDifference? {
let afterMarker = afterMarker ?? markers.first!.key
guard let subtreeStart = markers[afterMarker] else {
throw SubtreeError.invalidMarker(name: afterMarker)
}
guard
let subtree = SyntaxTypeFinder.findFirstNode(
in: actualTree,
afterUTF8Offset: subtreeStart,
ofType: baseline.syntaxNodeType
)
else {
throw SubtreeError.invalidSubtree(
tree: actualTree,
afterUTF8Offset: subtreeStart,
type: String(describing: baseline.syntaxNodeType)
)
}
return subtree.findFirstDifference(baseline: baseline, includeTrivia: includeTrivia)
}
/// Verifies that the subtree found from parsing the text passed into
/// `init(markedText:)` has the same structure as `expected`.
public func assertSameStructure(
afterMarker: String? = nil,
_ expected: some SyntaxProtocol,
includeTrivia: Bool = false,
additionalInfo: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) throws {
if let diff = try findFirstDifference(afterMarker: afterMarker, baseline: expected, includeTrivia: includeTrivia) {
let message: String
if let additionalInfo = additionalInfo {
message = """
\(additionalInfo)
\(diff.debugDescription)
"""
} else {
message = diff.debugDescription
}
XCTFail(message, file: file, line: line)
}
}
}
public enum SubtreeError: Error, CustomStringConvertible {
case invalidMarker(name: String)
case invalidSubtree(tree: Syntax, afterUTF8Offset: Int, type: String)
public var description: String {
switch self {
case .invalidMarker(let name):
return "Could not find marker with name '\(name)'"
case .invalidSubtree(let tree, let afterUTF8Offset, let type):
return
"Could not find subtree after UTF8 offset \(afterUTF8Offset) with type \(type) in:\n\(tree.debugDescription)"
}
}
}
fileprivate class SyntaxTypeFinder: SyntaxAnyVisitor {
private let offset: Int
private let type: SyntaxProtocol.Type
private var found: Syntax?
private init(offset: Int, type: SyntaxProtocol.Type) {
self.offset = offset
self.type = type
self.found = nil
super.init(viewMode: .all)
}
fileprivate override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
if found != nil || node.endPosition.utf8Offset < offset {
return .skipChildren
}
if node.positionAfterSkippingLeadingTrivia.utf8Offset >= offset && node.syntaxNodeType == type {
found = node
return .skipChildren
}
return .visitChildren
}
public static func findFirstNode(
in tree: Syntax,
afterUTF8Offset offset: Int,
ofType type: SyntaxProtocol.Type
) -> Syntax? {
let finder = SyntaxTypeFinder(offset: offset, type: type)
finder.walk(tree)
return finder.found
}
}