Skip to content

Commit b605766

Browse files
committed
Use emoji to mark the SourceEdits
This change is used to help us mark the changes in source file, which makes incremental parsing test easier.
1 parent 8311d8d commit b605766

File tree

4 files changed

+208
-48
lines changed

4 files changed

+208
-48
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 XCTest
15+
16+
/// Get `ConcurrentEdits` in source whose edited zones are marked with markers
17+
/// Also extract the markers from source to get original source and edited source
18+
///
19+
/// `⏩️` is *start marker*, `⏸️` is *separate marker*, `⏪️` is *end marker*
20+
/// Contents between `⏩️` and `⏸️` are source text that before modification, contents
21+
/// betwwen `⏸️` and `⏪️` are source text that after modification
22+
/// i.e. `⏩️foo⏸️bar⏪️`, the original source is `foo` and the edited source is `bar`
23+
public func getEditsAndSources(_ source: String) -> (edits: ConcurrentEdits, orignialSource: Substring, editedSource: Substring) {
24+
var editedSource = Substring()
25+
var originalSource = Substring()
26+
var concurrentEdits: [SourceEdit] = []
27+
28+
var lastStartIndex = source.startIndex
29+
while let startIndex = source[lastStartIndex...].firstIndex(where: { $0 == "⏩️" }),
30+
let separateIndex = source[startIndex...].firstIndex(where: { $0 == "⏸️" }),
31+
let endIndex = source[separateIndex...].firstIndex(where: { $0 == "⏪️" })
32+
{
33+
34+
originalSource += source[lastStartIndex..<startIndex]
35+
let edit = SourceEdit(
36+
offset: originalSource.utf8.count,
37+
length: source.utf8.distance(
38+
from: source.index(after: startIndex),
39+
to: separateIndex
40+
),
41+
replacementLength: source.utf8.distance(
42+
from: source.index(after: separateIndex),
43+
to: endIndex
44+
)
45+
)
46+
originalSource += source[source.index(after: startIndex)..<separateIndex]
47+
48+
editedSource += source[lastStartIndex..<startIndex] + source[source.index(after: separateIndex)..<endIndex]
49+
50+
concurrentEdits.append(edit)
51+
52+
lastStartIndex = source.index(after: endIndex)
53+
}
54+
55+
editedSource += source[lastStartIndex...]
56+
originalSource += source[lastStartIndex...]
57+
58+
do {
59+
let edits = try ConcurrentEdits(concurrent: concurrentEdits)
60+
return (edits, originalSource, editedSource)
61+
} catch {
62+
fatalError("ConcurrentEdits created by the test case do not satisfy ConcurrentEdits requirements, please check the test setup")
63+
}
64+
}
65+
66+
/// Apply the given edits to `testString` and return the resulting string.
67+
/// `concurrent` specifies whether the edits should be interpreted as being
68+
/// applied sequentially or concurrently.
69+
public func applyEdits(
70+
_ edits: [SourceEdit],
71+
concurrent: Bool,
72+
to testString: String,
73+
replacementChar: Character = "?"
74+
) -> String {
75+
guard let replacementAscii = replacementChar.asciiValue else {
76+
fatalError("replacementChar must be an ASCII character")
77+
}
78+
var edits = edits
79+
if concurrent {
80+
XCTAssert(ConcurrentEdits._isValidConcurrentEditArray(edits))
81+
82+
// If the edits are concurrent, sorted and not overlapping (as guaranteed by
83+
// the check above, we can apply them sequentially to the string in reverse
84+
// order because later edits don't affect earlier edits.
85+
edits = edits.reversed()
86+
}
87+
var bytes = Array(testString.utf8)
88+
for edit in edits {
89+
assert(edit.endOffset <= bytes.count)
90+
bytes.removeSubrange(edit.offset..<edit.endOffset)
91+
bytes.insert(contentsOf: [UInt8](repeating: replacementAscii, count: edit.replacementLength), at: edit.offset)
92+
}
93+
return String(bytes: bytes, encoding: .utf8)!
94+
}

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,43 @@
1313
import XCTest
1414
import SwiftSyntax
1515
import SwiftParser
16+
import _SwiftSyntaxTestSupport
1617

1718
public class IncrementalParsingTests: XCTestCase {
1819

1920
public func testIncrementalInvalid() {
20-
let original = "struct A { func f() {"
21-
let step: (String, (Int, Int, String)) =
22-
("struct AA { func f() {", (8, 0, "A"))
23-
24-
var tree = Parser.parse(source: original)
25-
let sourceEdit = SourceEdit(range: ByteSourceRange(offset: step.1.0, length: step.1.1), replacementLength: step.1.2.utf8.count)
26-
let lookup = IncrementalParseTransition(previousTree: tree, edits: ConcurrentEdits(sourceEdit))
27-
tree = Parser.parse(source: step.0, parseTransition: lookup)
28-
XCTAssertEqual("\(tree)", step.0)
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)
2932
}
3033

3134
public func testReusedNode() throws {
3235
try XCTSkipIf(true, "Swift parser does not handle node reuse yet")
3336

34-
let original = "struct A {}\nstruct B {}\n"
35-
let step: (String, (Int, Int, String)) =
36-
("struct AA {}\nstruct B {}\n", (8, 0, "A"))
37+
let source =
38+
"""
39+
struct A⏩️⏸️A⏪️ {}
40+
struct B {}
41+
"""
42+
43+
let (concurrentEdits, originalSubString, editedSubString) = getEditsAndSources(source)
44+
45+
let originalSource = String(originalSubString)
46+
let editedSource = String(editedSubString)
3747

38-
let origTree = Parser.parse(source: original)
39-
let sourceEdit = SourceEdit(range: ByteSourceRange(offset: step.1.0, length: step.1.1), replacementLength: step.1.2.utf8.count)
48+
let origTree = Parser.parse(source: originalSource)
4049
let reusedNodeCollector = IncrementalParseReusedNodeCollector()
41-
let transition = IncrementalParseTransition(previousTree: origTree, edits: ConcurrentEdits(sourceEdit), reusedNodeDelegate: reusedNodeCollector)
42-
let newTree = Parser.parse(source: step.0, parseTransition: transition)
43-
XCTAssertEqual("\(newTree)", step.0)
50+
let transition = IncrementalParseTransition(previousTree: origTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodeCollector)
51+
let newTree = Parser.parse(source: editedSource, parseTransition: transition)
52+
XCTAssertEqual("\(newTree)", editedSource)
4453

4554
let origStructB = origTree.statements[1]
4655
let newStructB = newTree.statements[1]

Tests/SwiftSyntaxTest/SequentialToConcurrentEditTranslationTests.swift

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import XCTest
1414
import SwiftSyntax
15+
import _SwiftSyntaxTestSupport
1516

1617
let longString = """
1718
1234567890abcdefghijklmnopqrstuvwxyz\
@@ -26,36 +27,6 @@ let longString = """
2627
1234567890abcdefghijklmnopqrstuvwzyz
2728
"""
2829

29-
/// Apply the given edits to `testString` and return the resulting string.
30-
/// `concurrent` specifies whether the edits should be interpreted as being
31-
/// applied sequentially or concurrently.
32-
func applyEdits(
33-
_ edits: [SourceEdit],
34-
concurrent: Bool,
35-
to testString: String = longString,
36-
replacementChar: Character = "?"
37-
) -> String {
38-
guard let replacementAscii = replacementChar.asciiValue else {
39-
fatalError("replacementChar must be an ASCII character")
40-
}
41-
var edits = edits
42-
if concurrent {
43-
XCTAssert(ConcurrentEdits._isValidConcurrentEditArray(edits))
44-
45-
// If the edits are concurrent, sorted and not overlapping (as guaranteed by
46-
// the check above, we can apply them sequentially to the string in reverse
47-
// order because later edits don't affect earlier edits.
48-
edits = edits.reversed()
49-
}
50-
var bytes = Array(testString.utf8)
51-
for edit in edits {
52-
assert(edit.endOffset <= bytes.count)
53-
bytes.removeSubrange(edit.offset..<edit.endOffset)
54-
bytes.insert(contentsOf: [UInt8](repeating: replacementAscii, count: edit.replacementLength), at: edit.offset)
55-
}
56-
return String(bytes: bytes, encoding: .utf8)!
57-
}
58-
5930
/// Verifies that
6031
/// 1. translation of the `sequential` edits results in the
6132
/// `expectedConcurrent` edits
@@ -364,7 +335,7 @@ final class TranslateSequentialToConcurrentEditsTests: XCTestCase {
364335
}
365336
print(edits)
366337
let normalizedEdits = ConcurrentEdits(fromSequential: edits)
367-
if applyEdits(edits, concurrent: false) != applyEdits(normalizedEdits.edits, concurrent: true) {
338+
if applyEdits(edits, concurrent: false, to: longString) != applyEdits(normalizedEdits.edits, concurrent: true, to: longString) {
368339
print("failed \(i)")
369340
fatalError()
370341
} else {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 SourceEditsUtilTest: XCTestCase {
18+
public func testGetConcurrentEdits() {
19+
let source =
20+
"""
21+
⏩️class⏸️struct⏪️ foo {
22+
init() {
23+
⏩️⏸️let bar = 10⏪️
24+
}
25+
26+
⏩️func bar() {}⏸️⏪️
27+
}
28+
"""
29+
30+
let (concurrentEdits, originalSource, _) = getEditsAndSources(source)
31+
32+
XCTAssertEqual(
33+
concurrentEdits.edits,
34+
[
35+
SourceEdit(offset: 0, length: 5, replacementLength: 6),
36+
SourceEdit(offset: 27, length: 0, replacementLength: 12),
37+
SourceEdit(offset: 35, length: 13, replacementLength: 0),
38+
]
39+
)
40+
41+
let expectedSource =
42+
"""
43+
?????? foo {
44+
init() {
45+
????????????
46+
}
47+
48+
49+
}
50+
"""
51+
52+
let sourceAppliedEdits = applyEdits(concurrentEdits.edits, concurrent: true, to: String(originalSource))
53+
54+
XCTAssertEqual(sourceAppliedEdits, expectedSource)
55+
}
56+
57+
public func testReplaceMultiByteCharWithShorter() {
58+
let source = "⏩️👨‍👩‍👧‍👦⏸️🎉⏪️"
59+
60+
let (concurrentEdits, originalSource, editedSource) = getEditsAndSources(source)
61+
62+
XCTAssertEqual(String(originalSource), "👨‍👩‍👧‍👦")
63+
XCTAssertEqual(String(editedSource), "🎉")
64+
XCTAssertEqual(
65+
concurrentEdits.edits,
66+
[
67+
SourceEdit(offset: 0, length: 25, replacementLength: 4)
68+
]
69+
)
70+
}
71+
72+
public func testReplaceWithMultiByteChar() {
73+
let source = "⏩️a⏸️👨‍👩‍👧‍👦⏪️"
74+
75+
let (concurrentEdits, originalSource, editedSource) = getEditsAndSources(source)
76+
77+
XCTAssertEqual(String(originalSource), "a")
78+
XCTAssertEqual(String(editedSource), "👨‍👩‍👧‍👦")
79+
XCTAssertEqual(
80+
concurrentEdits.edits,
81+
[
82+
SourceEdit(offset: 0, length: 1, replacementLength: 25)
83+
]
84+
)
85+
}
86+
}

0 commit comments

Comments
 (0)