Skip to content

Commit c62a189

Browse files
committed
Add utils for incremental parse test
1 parent 56ef324 commit c62a189

File tree

3 files changed

+280
-37
lines changed

3 files changed

+280
-37
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 verifyIncrementalParse(
23+
originalSource: String,
24+
editedSource: String,
25+
concurrentEdits: ConcurrentEdits,
26+
expectedReusedRanges: [(ByteSourceRange, SyntaxKind)]
27+
) {
28+
29+
let originalTree = Parser.parse(source: originalSource)
30+
31+
let reusedNodesCollector = IncrementalParseReusedNodeCollector()
32+
let transition = IncrementalParseTransition(previousTree: originalTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodesCollector)
33+
34+
let newTree = Parser.parse(source: editedSource)
35+
let incrementallyParsedNewTree = Parser.parse(source: editedSource, parseTransition: transition)
36+
37+
// Round-trip
38+
assertStringsEqualWithDiff(editedSource, "\(incrementallyParsedNewTree)")
39+
40+
// Substructure
41+
let subtreeMatcher = SubtreeMatcher(Syntax(incrementallyParsedNewTree), markers: [:])
42+
do {
43+
try subtreeMatcher.assertSameStructure(Syntax(newTree))
44+
} catch {
45+
XCTFail("Matching for a subtree failed with error: \(error)")
46+
}
47+
48+
// Re-used nodes
49+
// The range of node should ignore Trivia
50+
if expectedReusedRanges.isEmpty {
51+
XCTAssert(reusedNodesCollector.rangeAndNodes.isEmpty)
52+
} else {
53+
XCTAssertEqual(reusedNodesCollector.rangeAndNodes.count, expectedReusedRanges.count)
54+
55+
for (_, node) in reusedNodesCollector.rangeAndNodes {
56+
do {
57+
let range = ByteSourceRange(offset: node.positionAfterSkippingLeadingTrivia.utf8Offset, length: node.byteSizeAfterTrimmingTrivia)
58+
let targetKind = try XCTUnwrap(expectedReusedRanges.first(where: { $0.0 == range })).1
59+
XCTAssertEqual(targetKind, node.kind)
60+
} catch {
61+
XCTFail("Fail to find expected reused range")
62+
}
63+
}
64+
}
65+
}
66+
67+
/// Extract the markers in source string for incremental parse and return the information
68+
/// needed for test
69+
///
70+
/// `➡️` marks the start of reused range and `⬅️` marks the end of reused range
71+
/// e.g. `class foo {} ➡️struct bar {}⬅️`, the reused range is `struct bar {}`
72+
///
73+
/// - Returns:
74+
/// - reusedRange: Expected reused ranges in source, sorted by their offset
75+
/// - originalSource, editedSource, edits: Please refer to `getEditsAndSources`
76+
public func extractMarkersForIncrementParseTest(_ markedSource: String) -> (originalSource: String, editedSource: String, edits: ConcurrentEdits, reusedRange: [ByteSourceRange]) {
77+
var concurrentEdits: [SourceEdit] = []
78+
var originalSubStrings: [Substring] = []
79+
var editedSubStrings: [Substring] = []
80+
var reusedRange: [ByteSourceRange] = []
81+
82+
func extractEditsAndSources(_ subString: Substring) {
83+
let (edits, originalSource, editedSource) = getEditsAndSources(String(subString))
84+
85+
concurrentEdits.append(
86+
contentsOf:
87+
edits.edits.map {
88+
return SourceEdit(
89+
offset: originalSubStrings.map({ $0.utf8.count }).reduce(0, +) + $0.offset,
90+
length: $0.length,
91+
replacementLength: $0.replacementLength
92+
)
93+
}
94+
)
95+
96+
originalSubStrings.append(originalSource)
97+
editedSubStrings.append(editedSource)
98+
}
99+
100+
var lastStartIndex = markedSource.startIndex
101+
while let startIndex = markedSource[lastStartIndex...].firstIndex(of: "➡️"),
102+
let endIndex = markedSource[startIndex...].firstIndex(of: "⬅️")
103+
{
104+
105+
extractEditsAndSources(markedSource[lastStartIndex..<startIndex])
106+
107+
reusedRange.append(
108+
ByteSourceRange(
109+
offset: originalSubStrings.map({ $0.utf8.count }).reduce(0, +),
110+
length: markedSource.utf8.distance(from: markedSource.index(after: startIndex), to: endIndex)
111+
)
112+
)
113+
114+
extractEditsAndSources(markedSource[markedSource.index(after: startIndex)..<endIndex])
115+
116+
lastStartIndex = markedSource.index(after: endIndex)
117+
}
118+
119+
extractEditsAndSources(markedSource[lastStartIndex...])
120+
121+
do {
122+
let edits = try ConcurrentEdits(concurrent: concurrentEdits)
123+
return (originalSubStrings.joined(), editedSubStrings.joined(), edits, reusedRange.sorted(by: { $0.offset < $1.offset }))
124+
} catch {
125+
fatalError("ConcurrentEdits created by the test case do not satisfy ConcurrentEdits requirements, please check the test setup")
126+
}
127+
}

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ public class IncrementalParsingTests: XCTestCase {
1919

2020
public func testIncrementalInvalid() {
2121
let source = "struct A⏩️⏸️A⏪️ { func f() {"
22-
let (concurrentEdits, originalSubString, editedSubString) = getEditsAndSources(source)
2322

24-
let originalSource = String(originalSubString)
25-
let editedSource = String(editedSubString)
23+
let (originalSource, editedSource, edits, _) = extractMarkersForIncrementParseTest(source)
2624

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)
25+
verifyIncrementalParse(
26+
originalSource: originalSource,
27+
editedSource: editedSource,
28+
concurrentEdits: edits,
29+
expectedReusedRanges: []
30+
)
3231
}
3332

3433
public func testReusedNode() throws {
@@ -37,36 +36,23 @@ public class IncrementalParsingTests: XCTestCase {
3736
let source =
3837
"""
3938
struct A⏩️⏸️A⏪️ {}
40-
struct B {}
39+
➡️struct B {}⬅️
4140
"""
4241

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)
42+
let (originalSource, editedSource, edits, reusedRange) = extractMarkersForIncrementParseTest(source)
43+
44+
verifyIncrementalParse(
45+
originalSource: originalSource,
46+
editedSource: editedSource,
47+
concurrentEdits: edits,
48+
expectedReusedRanges: Array(
49+
zip(
50+
reusedRange,
51+
[
52+
.codeBlockItem
53+
]
54+
)
55+
)
56+
)
7157
}
7258
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 XCTest
14+
import SwiftSyntax
15+
import _SwiftSyntaxTestSupport
16+
17+
public class IncrementalParseTestUtilsTest: XCTestCase {
18+
public func testReuseRangeBeforeEditedArea() {
19+
let source =
20+
"""
21+
➡️struct B {}⬅️
22+
struct A⏩️⏸️A⏪️ {}
23+
"""
24+
25+
let expectedOriginalSource =
26+
"""
27+
struct B {}
28+
struct A {}
29+
"""
30+
31+
let expectedEditedSource =
32+
"""
33+
struct B {}
34+
struct AA {}
35+
"""
36+
37+
let (originalSource, editedSource, edits, reusedRange) = extractMarkersForIncrementParseTest(source)
38+
XCTAssertEqual(originalSource, expectedOriginalSource)
39+
XCTAssertEqual(editedSource, expectedEditedSource)
40+
XCTAssertEqual(
41+
edits.edits,
42+
[
43+
SourceEdit(offset: 20, length: 0, replacementLength: 1)
44+
]
45+
)
46+
XCTAssertEqual(
47+
reusedRange,
48+
[
49+
ByteSourceRange(offset: 0, length: 11)
50+
]
51+
)
52+
}
53+
54+
public func testReuseRangeAfterEditedArea() {
55+
let source =
56+
"""
57+
struct A⏩️⏸️A⏪️ {}
58+
➡️struct B {}⬅️
59+
"""
60+
61+
let expectedOriginalSource =
62+
"""
63+
struct A {}
64+
struct B {}
65+
"""
66+
67+
let expectedEditedSource =
68+
"""
69+
struct AA {}
70+
struct B {}
71+
"""
72+
73+
let (originalSource, editedSource, edits, reusedRange) = extractMarkersForIncrementParseTest(source)
74+
XCTAssertEqual(originalSource, expectedOriginalSource)
75+
XCTAssertEqual(editedSource, expectedEditedSource)
76+
XCTAssertEqual(
77+
edits.edits,
78+
[
79+
SourceEdit(offset: 8, length: 0, replacementLength: 1)
80+
]
81+
)
82+
XCTAssertEqual(
83+
reusedRange,
84+
[
85+
ByteSourceRange(offset: 12, length: 11)
86+
]
87+
)
88+
}
89+
90+
public func testReuseRangeInsideMultipleEdits() {
91+
let source =
92+
"""
93+
⏩️⏸️class foo {⏪️
94+
➡️let value1 = 👨‍👩‍👧‍👧⬅️
95+
⏩️⏸️}⏪️
96+
"""
97+
98+
let expectedOriginalSource =
99+
"""
100+
101+
let value1 = 👨‍👩‍👧‍👧
102+
103+
"""
104+
105+
let expectedEditedSource =
106+
"""
107+
class foo {
108+
let value1 = 👨‍👩‍👧‍👧
109+
}
110+
"""
111+
112+
let (originalSource, editedSource, edits, reusedRange) = extractMarkersForIncrementParseTest(source)
113+
XCTAssertEqual(originalSource, expectedOriginalSource)
114+
XCTAssertEqual(editedSource, expectedEditedSource)
115+
XCTAssertEqual(
116+
edits.edits,
117+
[
118+
SourceEdit(offset: 0, length: 0, replacementLength: 11),
119+
SourceEdit(offset: 42, length: 0, replacementLength: 1),
120+
]
121+
)
122+
XCTAssertEqual(
123+
reusedRange,
124+
[
125+
ByteSourceRange(offset: 3, length: 38)
126+
]
127+
)
128+
129+
}
130+
}

0 commit comments

Comments
 (0)