Skip to content

Commit 96e0fe0

Browse files
committed
In AssertParse, allow asserting for a substructure that should occur in a syntax tree
1 parent ae2a11d commit 96e0fe0

File tree

4 files changed

+133
-149
lines changed

4 files changed

+133
-149
lines changed

Sources/SwiftParser/SwiftParser.docc/FixingBugs.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,14 @@ Diagnostics are produced when the parsed syntax tree contains missing or unexpec
2727

2828
1. Add a test case in `SwiftParserTest` that looks like the following
2929
```swift
30-
let source = """
31-
<#your code that produces an invalid syntax tree#>
32-
"""
33-
34-
let tree = withParser(source: source) {
35-
Syntax(raw: $0.parseSourceFile().raw)
36-
}
37-
XCTAssertHasSubstructure(
38-
tree,
39-
<#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#>
4035
)
4136
```
42-
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`
43-
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.
4438

4539
## Unhelpful Diagnostic Produced
4640

Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift

Lines changed: 5 additions & 56 deletions
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
@@ -141,8 +87,8 @@ public struct SubtreeMatcher {
14187
self.actualTree = try parse(text)
14288
}
14389

144-
public init(_ actualTree: Syntax) {
145-
self.markers = ["DEFAULT": 0]
90+
public init(_ actualTree: Syntax, markers: [String: Int]) {
91+
self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers
14692
self.actualTree = actualTree
14793
}
14894

@@ -164,6 +110,9 @@ public struct SubtreeMatcher {
164110

165111
/// Same as `XCTAssertSameStructure`, but uses the subtree found from parsing
166112
/// the text passed into `init(markedText:)` as the `actual` tree.
113+
114+
/// Verifies that the the subtree found from parsing the text passed into
115+
/// `init(markedText:)` has the same structure as `expected`.
167116
public func assertSameStructure(afterMarker: String? = nil, _ expected: Syntax, includeTrivia: Bool = false,
168117
file: StaticString = #filePath, line: UInt = #line) throws {
169118
let diff = try findFirstDifference(afterMarker: afterMarker, baseline: expected, includeTrivia: includeTrivia)

Tests/SwiftParserTest/Assertions.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,11 @@ func AssertDiagnostic<T: SyntaxProtocol>(
160160
/// These markers are removed before parsing the source file.
161161
/// By default, `DiagnosticSpec` asserts that the diagnostics is produced at a location marked by `#^DIAG^#`.
162162
/// `parseSyntax` can be used to adjust the production that should be used as the entry point to parse the source code.
163+
/// If `substructure` is not `nil`, asserts that the parsed syntax tree contains this substructure.
163164
func AssertParse<Node: RawSyntaxNodeProtocol>(
164165
_ markedSource: String,
165166
_ parseSyntax: (inout Parser) -> Node = { $0.parseSourceFile() },
167+
substructure expectedSubstructure: Syntax? = nil,
166168
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
167169
fixedSource expectedFixedSource: String? = nil,
168170
file: StaticString = #file,
@@ -175,12 +177,26 @@ func AssertParse<Node: RawSyntaxNodeProtocol>(
175177
var parser = Parser(buf)
176178
withExtendedLifetime(parser) {
177179
let tree = Syntax(raw: parseSyntax(&parser).raw)
180+
181+
// Round-trip
178182
AssertStringsEqualWithDiff("\(tree)", source, additionalInfo: """
179183
Source failed to round-trip.
180184
181185
Actual syntax tree:
182186
\(tree.recursiveDescription)
183187
""", file: file, line: line)
188+
189+
// Substructure
190+
if let expectedSubstructure = expectedSubstructure {
191+
let subtreeMatcher = SubtreeMatcher(Syntax(tree), markers: [:])
192+
do {
193+
try subtreeMatcher.assertSameStructure(Syntax(expectedSubstructure), file: file, line: line)
194+
} catch {
195+
XCTFail("Matching for a subtree failed with error: \(error)", file: file, line: line)
196+
}
197+
}
198+
199+
// Diagnostics
184200
let diags = ParseDiagnosticsGenerator.diagnostics(for: tree)
185201
XCTAssertEqual(diags.count, expectedDiagnostics.count, """
186202
Expected \(expectedDiagnostics.count) diagnostics but received \(diags.count):
@@ -189,6 +205,8 @@ func AssertParse<Node: RawSyntaxNodeProtocol>(
189205
for (diag, expectedDiag) in zip(diags, expectedDiagnostics) {
190206
AssertDiagnostic(diag, in: tree, markerLocations: markerLocations, expected: expectedDiag, file: file, line: line)
191207
}
208+
209+
// Applying Fix-Its
192210
if let expectedFixedSource = expectedFixedSource {
193211
let fixedSource = FixItApplier.applyFixes(in: diags, to: tree).description
194212
AssertStringsEqualWithDiff(fixedSource, expectedFixedSource, file: file, line: line)

Tests/SwiftParserTest/RecoveryTests.swift

Lines changed: 104 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import XCTest
44
import _SwiftSyntaxTestSupport
55

66
public class RecoveryTests: XCTestCase {
7-
func testRecoverOneExtraLabel() throws {
8-
try XCTAssertHasSubstructure(
9-
"(first second third: Int)",
10-
parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } },
11-
FunctionParameterSyntax(
7+
func testRecoverOneExtraLabel() {
8+
AssertParse(
9+
"(first second #^DIAG^#third: Int)",
10+
{ $0.parseFunctionSignature() },
11+
substructure: Syntax(FunctionParameterSyntax(
1212
attributes: nil,
1313
firstName: TokenSyntax.identifier("first"),
1414
secondName: TokenSyntax.identifier("second"),
@@ -18,15 +18,18 @@ public class RecoveryTests: XCTestCase {
1818
ellipsis: nil,
1919
defaultArgument: nil,
2020
trailingComma: nil
21-
)
21+
)),
22+
diagnostics: [
23+
DiagnosticSpec(message: "Unexpected text 'third' found in function parameter")
24+
]
2225
)
2326
}
2427

25-
func testRecoverTwoExtraLabels() throws {
26-
try XCTAssertHasSubstructure(
27-
"(first second third fourth: Int)",
28-
parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } },
29-
FunctionParameterSyntax(
28+
func testRecoverTwoExtraLabels() {
29+
AssertParse(
30+
"(first second #^DIAG^#third fourth: Int)",
31+
{ $0.parseFunctionSignature() },
32+
substructure: Syntax(FunctionParameterSyntax(
3033
attributes: nil,
3134
firstName: TokenSyntax.identifier("first"),
3235
secondName: TokenSyntax.identifier("second"),
@@ -36,29 +39,42 @@ public class RecoveryTests: XCTestCase {
3639
ellipsis: nil,
3740
defaultArgument: nil,
3841
trailingComma: nil
39-
)
42+
)),
43+
diagnostics: [
44+
DiagnosticSpec(message: "Unexpected text 'third fourth' found in function parameter")
45+
]
4046
)
4147
}
4248

4349
func testDontRecoverFromDeclKeyword() {
44-
var source = """
45-
(first second third struct: Int)
46-
"""
47-
let (_, currentToken) = source.withUTF8 { buffer in
48-
var parser = Parser(buffer)
49-
return (parser.parseFunctionSignature(), parser.currentToken)
50-
}
51-
52-
// The 'struct' keyword should be taken as an indicator that a new decl
53-
// starts here, so `parseFunctionSignature` shouldn't eat it.
54-
XCTAssertEqual(currentToken.tokenKind, .structKeyword)
50+
AssertParse(
51+
"func foo(first second #^MISSING_COLON^#third #^MISSING_RPAREN^#struct#^MISSING_IDENTIFIER^##^BRACES^#: Int) {}",
52+
substructure: Syntax(FunctionParameterSyntax(
53+
attributes: nil,
54+
firstName: .identifier("first"),
55+
secondName: .identifier("second"),
56+
colon: .colonToken(presence: .missing),
57+
type: TypeSyntax(SimpleTypeIdentifierSyntax(name: .identifier("third"), genericArgumentClause: nil)),
58+
ellipsis: nil,
59+
defaultArgument: nil,
60+
trailingComma: nil
61+
)),
62+
diagnostics: [
63+
DiagnosticSpec(locationMarker: "MISSING_COLON", message: "Expected ':' in function parameter"),
64+
DiagnosticSpec(locationMarker: "MISSING_RPAREN", message: "Expected ')' to end parameter clause"),
65+
// FIXME: We should issues something like "Expected identifier in declaration"
66+
DiagnosticSpec(locationMarker: "MISSING_IDENTIFIER", message: "Expected '' in declaration"),
67+
DiagnosticSpec(locationMarker: "BRACES", message: "Expected '{'"),
68+
DiagnosticSpec(locationMarker: "BRACES", message: "Expected '}'"),
69+
]
70+
)
5571
}
5672

57-
func testRecoverFromParens() throws {
58-
try XCTAssertHasSubstructure(
59-
"(first second [third fourth]: Int)",
60-
parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } },
61-
FunctionParameterSyntax(
73+
func testRecoverFromParens() {
74+
AssertParse(
75+
"(first second #^DIAG^#[third fourth]: Int)",
76+
{ $0.parseFunctionSignature() },
77+
substructure: Syntax(FunctionParameterSyntax(
6278
attributes: nil,
6379
firstName: TokenSyntax.identifier("first"),
6480
secondName: TokenSyntax.identifier("second"),
@@ -73,67 +89,71 @@ public class RecoveryTests: XCTestCase {
7389
ellipsis: nil,
7490
defaultArgument: nil,
7591
trailingComma: nil
76-
)
92+
)),
93+
diagnostics: [
94+
DiagnosticSpec(message: "Unexpected text '[third fourth]' found in function parameter")
95+
]
7796
)
7897
}
7998

80-
func testDontRecoverFromUnbalancedParens() throws {
81-
let source = """
82-
(first second [third fourth: Int)
83-
"""
84-
try withParser(source: source) { parser in
85-
let signature = Syntax(raw: parser.parseFunctionSignature().raw)
86-
let currentToken = parser.currentToken
87-
XCTAssertEqual(currentToken.tokenKind, .identifier)
88-
XCTAssertEqual(currentToken.tokenText, "fourth")
89-
try XCTAssertHasSubstructure(
90-
signature,
91-
FunctionParameterSyntax(
92-
attributes: nil,
93-
firstName: TokenSyntax.identifier("first"),
94-
secondName: TokenSyntax.identifier("second"),
95-
colon: TokenSyntax(.colon, presence: .missing),
96-
type: TypeSyntax(ArrayTypeSyntax(
97-
leftSquareBracket: TokenSyntax.leftSquareBracketToken(),
98-
elementType: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)),
99-
rightSquareBracket: TokenSyntax(.rightSquareBracket, presence: .missing)
100-
)),
101-
ellipsis: nil,
102-
defaultArgument: nil,
103-
trailingComma: nil
104-
)
105-
)
106-
}
99+
func testDontRecoverFromUnbalancedParens() {
100+
AssertParse(
101+
"func foo(first second #^COLON^#[third #^RSQUARE_COLON^#fourth: Int) {}",
102+
substructure: Syntax(FunctionParameterSyntax(
103+
attributes: nil,
104+
firstName: TokenSyntax.identifier("first"),
105+
secondName: TokenSyntax.identifier("second"),
106+
colon: TokenSyntax(.colon, presence: .missing),
107+
type: TypeSyntax(ArrayTypeSyntax(
108+
leftSquareBracket: TokenSyntax.leftSquareBracketToken(),
109+
elementType: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)),
110+
rightSquareBracket: TokenSyntax(.rightSquareBracket, presence: .missing)
111+
)),
112+
ellipsis: nil,
113+
defaultArgument: nil,
114+
trailingComma: nil
115+
)),
116+
diagnostics: [
117+
DiagnosticSpec(locationMarker: "COLON", message: "Expected ':' in function parameter"),
118+
DiagnosticSpec(locationMarker: "RSQUARE_COLON" , message: "Expected ']' to end type"),
119+
DiagnosticSpec(locationMarker: "RSQUARE_COLON", message: "Expected ')' to end parameter clause"),
120+
]
121+
)
107122
}
108123

109124
func testDontRecoverIfNewlineIsBeforeColon() {
110-
var source = """
111-
(first second third
112-
: Int)
113-
"""
114-
let (_, currentToken) = source.withUTF8 { buffer in
115-
var parser = Parser(buffer)
116-
return (parser.parseFunctionSignature(), parser.currentToken)
117-
}
118-
119-
XCTAssertEqual(currentToken.tokenKind, .colon)
125+
AssertParse(
126+
"""
127+
func foo(first second #^COLON^#third#^RPAREN^#
128+
: Int) {}
129+
""",
130+
substructure: Syntax(FunctionParameterSyntax(
131+
attributes: nil,
132+
firstName: TokenSyntax.identifier("first"),
133+
secondName: TokenSyntax.identifier("second"),
134+
colon: TokenSyntax(.colon, presence: .missing),
135+
type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)),
136+
ellipsis: nil,
137+
defaultArgument: nil,
138+
trailingComma: nil
139+
)),
140+
diagnostics: [
141+
DiagnosticSpec(locationMarker: "COLON", message: "Expected ':' in function parameter"),
142+
DiagnosticSpec(locationMarker: "RPAREN", message: "Expected ')' to end parameter clause"),
143+
]
144+
)
120145
}
121146

122-
public func testNoParamsForFunction() throws {
123-
let source = """
124-
class MyClass {
125-
func withoutParameters
126-
127-
func withParameters() {}
128-
}
129-
"""
147+
public func testNoParamsForFunction() {
148+
AssertParse(
149+
"""
150+
class MyClass {
151+
func withoutParameters#^DIAG^#
130152
131-
let classDecl = withParser(source: source) {
132-
Syntax(raw: $0.parseDeclaration().raw)
133-
}
134-
try XCTAssertHasSubstructure(
135-
classDecl,
136-
FunctionDeclSyntax(
153+
func withParameters() {}
154+
}
155+
""",
156+
substructure: Syntax(FunctionDeclSyntax(
137157
attributes: nil,
138158
modifiers: nil,
139159
funcKeyword: .funcKeyword(),
@@ -151,7 +171,10 @@ public class RecoveryTests: XCTestCase {
151171
),
152172
genericWhereClause: nil,
153173
body: nil
154-
)
174+
)),
175+
diagnostics: [
176+
DiagnosticSpec(message: "Expected argument list in function declaration")
177+
]
155178
)
156179
}
157180
}

0 commit comments

Comments
 (0)