Skip to content

Commit 09bb68a

Browse files
committed
Add assertIncrementalParse for incremental parse test
1 parent 6d65e39 commit 09bb68a

File tree

2 files changed

+150
-45
lines changed

2 files changed

+150
-45
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
if reusedNodesCollector.rangeAndNodes.count != expectedReusedNodes.count {
65+
XCTFail(
66+
"""
67+
Expected \(expectedReusedNodes.count) re-used nodes but received \(reusedNodesCollector.rangeAndNodes.count):
68+
\(reusedNodesCollector.rangeAndNodes.map {$0.1.description}.joined(separator: "\n"))
69+
""",
70+
file: file,
71+
line: line
72+
)
73+
return
74+
}
75+
76+
for expectedReusedNode in expectedReusedNodes {
77+
guard let range = getByteSourceRange(for: expectedReusedNode.source, in: originalString) else {
78+
XCTFail("Fail to find string in original source,", file: expectedReusedNode.file, line: expectedReusedNode.line)
79+
continue
80+
}
81+
82+
guard let reusedNode = reusedNodesCollector.rangeAndNodes.first(where: { $0.0 == range })?.1 else {
83+
XCTFail(
84+
"""
85+
Fail to match the range of \(expectedReusedNode.source) in:
86+
\(reusedNodesCollector.rangeAndNodes.map({"\($0.0): \($0.1.description)"}).joined(separator: "\n"))
87+
""",
88+
file: expectedReusedNode.file,
89+
line: expectedReusedNode.line
90+
)
91+
continue
92+
}
93+
94+
XCTAssertEqual(
95+
expectedReusedNode.kind,
96+
expectedReusedNode.kind,
97+
"""
98+
Expected \(expectedReusedNode.kind) syntax kind but received \(reusedNode.kind)
99+
""",
100+
file: expectedReusedNode.file,
101+
line: expectedReusedNode.line
102+
)
103+
}
104+
}
105+
106+
fileprivate func getByteSourceRange(for substring: String, in sourceString: String) -> ByteSourceRange? {
107+
if let range = sourceString.range(of: substring) {
108+
return ByteSourceRange(
109+
offset: sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound),
110+
length: sourceString.utf8.distance(from: range.lowerBound, to: range.upperBound)
111+
)
112+
}
113+
return nil
114+
}
115+
116+
/// An abstract data structure to describe the how a re-used node produced by the incremental parse should look like.
117+
public struct ReusedNodeSpec {
118+
/// The re-used string in original source without any ``Trivia``
119+
let source: String
120+
/// The ``SyntaxKind`` of re-used node
121+
let kind: SyntaxKind
122+
/// The file and line at which this ``ReusedNodeSpec`` was created, so that assertion failures can be reported at its location.
123+
let file: StaticString
124+
let line: UInt
125+
126+
public init(
127+
_ source: String,
128+
kind: SyntaxKind,
129+
file: StaticString = #file,
130+
line: UInt = #line
131+
) {
132+
self.source = source
133+
self.kind = kind
134+
self.file = file
135+
self.line = line
136+
}
137+
}

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)