Skip to content

Commit 7548b83

Browse files
committed
Add assertIncrementalParse for incremental parse test
1 parent 56ef324 commit 7548b83

File tree

2 files changed

+148
-45
lines changed

2 files changed

+148
-45
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
import SwiftSyntax
14+
import SwiftParser
15+
import XCTest
16+
17+
/// This function is used to verify the correctness of incremental parsing
18+
/// containing three parts:
19+
/// 1. Round-trip on the incrementally parsed syntax tree.
20+
/// 2. verify that incrementally parsing the edited source base on the original source produces the same syntax tree as reparsing the post-edit file from scratch.
21+
/// 3. verify the reused nodes fall into expectations.
22+
public func assertIncrementalParse(
23+
_ source: String,
24+
reusedNodes expectedReusedNodes: [ReusedNodeSpec] = [],
25+
file: StaticString = #file,
26+
line: UInt = #line
27+
) {
28+
let (concurrentEdits, originalSource, editedSource) = getEditsAndSources(source)
29+
30+
let originalString = String(originalSource)
31+
let editedString = String(editedSource)
32+
33+
let originalTree = Parser.parse(source: originalString)
34+
35+
let reusedNodesCollector = IncrementalParseReusedNodeCollector()
36+
let transition = IncrementalParseTransition(previousTree: originalTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodesCollector)
37+
38+
let newTree = Parser.parse(source: editedString)
39+
let incrementallyParsedNewTree = Parser.parse(source: editedString, parseTransition: transition)
40+
41+
// Round-trip
42+
assertStringsEqualWithDiff(
43+
editedString,
44+
"\(incrementallyParsedNewTree)",
45+
additionalInfo: """
46+
Source failed to round-trip when parsing incrementally
47+
48+
Actual syntax tree:
49+
\(incrementallyParsedNewTree.debugDescription)
50+
""",
51+
file: file,
52+
line: line
53+
)
54+
55+
// Substructure
56+
let subtreeMatcher = SubtreeMatcher(Syntax(incrementallyParsedNewTree), markers: [:])
57+
do {
58+
try subtreeMatcher.assertSameStructure(Syntax(newTree), includeTrivia: true, file: file, line: line)
59+
} catch {
60+
XCTFail("Matching for a subtree failed with error: \(error)", file: file, line: line)
61+
}
62+
63+
// Re-used nodes
64+
XCTAssertEqual(
65+
reusedNodesCollector.rangeAndNodes.count,
66+
expectedReusedNodes.count,
67+
"""
68+
Expected \(expectedReusedNodes.count) re-used nodes but received \(reusedNodesCollector.rangeAndNodes.count)
69+
""",
70+
file: file,
71+
line: line
72+
)
73+
74+
for targetNode in expectedReusedNodes {
75+
guard let range = getByteSourceRange(for: targetNode.source, in: originalString) else {
76+
XCTFail("Fail to find string in original source,", file: targetNode.file, line: targetNode.line)
77+
continue
78+
}
79+
80+
guard let reusedNode = reusedNodesCollector.rangeAndNodes.first(where: { $0.0 == range })?.1 else {
81+
XCTFail(
82+
"""
83+
Fail to match the range of \(targetNode.source) in:
84+
\(reusedNodesCollector.rangeAndNodes.map({"\($0.0): \($0.1.description)"}).joined(separator: "\n"))
85+
""",
86+
file: targetNode.file,
87+
line: targetNode.line
88+
)
89+
continue
90+
}
91+
92+
XCTAssertEqual(
93+
targetNode.kind,
94+
reusedNode.kind,
95+
"""
96+
Expected \(targetNode.kind) syntax kind but received \(reusedNode.kind)
97+
""",
98+
file: targetNode.file,
99+
line: targetNode.line
100+
)
101+
}
102+
}
103+
104+
fileprivate func getByteSourceRange(for substring: String, in sourceString: String) -> ByteSourceRange? {
105+
if let range = sourceString.range(of: substring) {
106+
return ByteSourceRange(
107+
offset: sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound),
108+
length: sourceString.utf8.distance(from: range.lowerBound, to: range.upperBound)
109+
)
110+
}
111+
return nil
112+
}
113+
114+
/// An abstract data structure to describe the how a re-used node produced by the incremental parse should look like.
115+
public struct ReusedNodeSpec {
116+
/// The re-used string in original source without any `Trivia`
117+
let source: String
118+
/// The `SyntaxKind` of re-used node
119+
let kind: SyntaxKind
120+
/// The file and line at which this `ReusedNodeSpec` was created, so that assertion failures can be reported at its location.
121+
let file: StaticString
122+
let line: UInt
123+
124+
public init(
125+
_ source: String,
126+
kind: SyntaxKind,
127+
file: StaticString = #file,
128+
line: UInt = #line
129+
) {
130+
self.source = source
131+
self.kind = kind
132+
self.file = file
133+
self.line = line
134+
}
135+
}

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,55 +18,23 @@ import _SwiftSyntaxTestSupport
1818
public class IncrementalParsingTests: XCTestCase {
1919

2020
public func testIncrementalInvalid() {
21-
let source = "struct A⏩️⏸️A⏪️ { func f() {"
22-
let (concurrentEdits, originalSubString, editedSubString) = getEditsAndSources(source)
23-
24-
let originalSource = String(originalSubString)
25-
let editedSource = String(editedSubString)
26-
27-
var tree = Parser.parse(source: originalSource)
28-
29-
let lookup = IncrementalParseTransition(previousTree: tree, edits: concurrentEdits)
30-
tree = Parser.parse(source: editedSource, parseTransition: lookup)
31-
XCTAssertEqual("\(tree)", editedSource)
21+
assertIncrementalParse(
22+
"""
23+
struct A⏩️⏸️A⏪️ { func f() {
24+
"""
25+
)
3226
}
3327

34-
public func testReusedNode() throws {
35-
try XCTSkipIf(true, "Swift parser does not handle node reuse yet")
36-
37-
let source =
28+
public func testReusedNode() {
29+
XCTExpectFailure()
30+
assertIncrementalParse(
3831
"""
3932
struct A⏩️⏸️A⏪️ {}
4033
struct B {}
41-
"""
42-
43-
let (concurrentEdits, originalSubString, editedSubString) = getEditsAndSources(source)
44-
45-
let originalSource = String(originalSubString)
46-
let editedSource = String(editedSubString)
47-
48-
let origTree = Parser.parse(source: originalSource)
49-
let reusedNodeCollector = IncrementalParseReusedNodeCollector()
50-
let transition = IncrementalParseTransition(previousTree: origTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodeCollector)
51-
let newTree = Parser.parse(source: editedSource, parseTransition: transition)
52-
XCTAssertEqual("\(newTree)", editedSource)
53-
54-
let origStructB = origTree.statements[1]
55-
let newStructB = newTree.statements[1]
56-
XCTAssertEqual("\(origStructB)", "\nstruct B {}")
57-
XCTAssertEqual("\(newStructB)", "\nstruct B {}")
58-
XCTAssertNotEqual(origStructB, newStructB)
59-
60-
XCTAssertEqual(reusedNodeCollector.rangeAndNodes.count, 1)
61-
if reusedNodeCollector.rangeAndNodes.count != 1 { return }
62-
let (reusedRange, reusedNode) = reusedNodeCollector.rangeAndNodes[0]
63-
XCTAssertEqual("\(reusedNode)", "\nstruct B {}")
64-
65-
XCTAssertEqual(newStructB.byteRange, reusedRange)
66-
XCTAssertEqual(origStructB.id, reusedNode.id)
67-
XCTAssertEqual(origStructB, reusedNode.as(CodeBlockItemSyntax.self))
68-
XCTAssertTrue(reusedNode.is(CodeBlockItemSyntax.self))
69-
XCTAssertEqual(origStructB, reusedNode.as(CodeBlockItemSyntax.self)!)
70-
XCTAssertEqual(origStructB.parent!.id, reusedNode.parent!.id)
34+
""",
35+
reusedNodes: [
36+
ReusedNodeSpec("struct B {}", kind: .codeBlockItem)
37+
]
38+
)
7139
}
7240
}

0 commit comments

Comments
 (0)