Skip to content

Commit dcc35e7

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 dcc35e7

File tree

4 files changed

+177
-48
lines changed

4 files changed

+177
-48
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
public func getConcurrentEditsAndSources(_ source: String) -> (edits: ConcurrentEdits, orignialSource: String, editedSource: String) {
19+
var editedSource = source
20+
21+
var sequentialEdits: [SourceEdit] = []
22+
while let (edit, afterSource) = nextMarkedEdits(editedSource) {
23+
sequentialEdits.append(edit)
24+
editedSource = afterSource
25+
}
26+
27+
return (ConcurrentEdits(fromSequential: sequentialEdits), extractOriginalSource(from: source), editedSource)
28+
}
29+
30+
fileprivate func nextMarkedEdits(_ source: String) -> (edit: SourceEdit, editedSource: String)? {
31+
guard let startIndex = source.firstIndex(where: { $0.isStartMarker }),
32+
let separateIndex = source.firstIndex(where: { $0.isSeperateMarker }),
33+
let endIndex = source.firstIndex(where: { $0.isEndMarker })
34+
else {
35+
return nil
36+
}
37+
38+
let edit = SourceEdit(offset: source.distance(from: source.startIndex, to: startIndex), length: source.distance(from: source.index(after: startIndex), to: separateIndex), replacementLength: source.distance(from: source.index(after: separateIndex), to: endIndex))
39+
40+
let editedSource = String(source[source.startIndex..<startIndex] + source[source.index(after: separateIndex)..<endIndex] + source[source.index(after: endIndex)...])
41+
42+
return (edit, editedSource)
43+
}
44+
45+
fileprivate func extractOriginalSource(from source: String) -> String {
46+
var text = source
47+
while let startIdx = text.firstIndex(where: { $0.isStartMarker }),
48+
let separateIdx = text.firstIndex(where: { $0.isSeperateMarker }),
49+
let endIdx = text.firstIndex(where: { $0.isEndMarker })
50+
{
51+
text = String(text[text.startIndex..<startIdx] + text[text.index(after: startIdx)..<separateIdx] + text[text.index(after: endIdx)...])
52+
}
53+
54+
return text
55+
}
56+
57+
/// Apply the given edits to `testString` and return the resulting string.
58+
/// `concurrent` specifies whether the edits should be interpreted as being
59+
/// applied sequentially or concurrently.
60+
public func applyEdits(
61+
_ edits: [SourceEdit],
62+
concurrent: Bool,
63+
to testString: String,
64+
replacementChar: Character = "?"
65+
) -> String {
66+
guard let replacementAscii = replacementChar.asciiValue else {
67+
fatalError("replacementChar must be an ASCII character")
68+
}
69+
var edits = edits
70+
if concurrent {
71+
XCTAssert(ConcurrentEdits._isValidConcurrentEditArray(edits))
72+
73+
// If the edits are concurrent, sorted and not overlapping (as guaranteed by
74+
// the check above, we can apply them sequentially to the string in reverse
75+
// order because later edits don't affect earlier edits.
76+
edits = edits.reversed()
77+
}
78+
var bytes = Array(testString.utf8)
79+
for edit in edits {
80+
assert(edit.endOffset <= bytes.count)
81+
bytes.removeSubrange(edit.offset..<edit.endOffset)
82+
bytes.insert(contentsOf: [UInt8](repeating: replacementAscii, count: edit.replacementLength), at: edit.offset)
83+
}
84+
return String(bytes: bytes, encoding: .utf8)!
85+
}
86+
87+
fileprivate extension Character {
88+
var isStartMarker: Bool {
89+
self == "⏩️"
90+
}
91+
92+
var isSeperateMarker: Bool {
93+
self == "⏸️"
94+
}
95+
96+
var isEndMarker: Bool {
97+
self == "⏪️"
98+
}
99+
}

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,37 @@
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, originalSource, editedSource) = getConcurrentEditsAndSources(source)
23+
24+
var tree = Parser.parse(source: originalSource)
25+
26+
let lookup = IncrementalParseTransition(previousTree: tree, edits: concurrentEdits)
27+
tree = Parser.parse(source: editedSource, parseTransition: lookup)
28+
XCTAssertEqual("\(tree)", editedSource)
2929
}
3030

3131
public func testReusedNode() throws {
3232
try XCTSkipIf(true, "Swift parser does not handle node reuse yet")
3333

34-
let original = "struct A {}\nstruct B {}\n"
35-
let step: (String, (Int, Int, String)) =
36-
("struct AA {}\nstruct B {}\n", (8, 0, "A"))
34+
let source =
35+
"""
36+
struct A⏩️⏸️A⏪️ {}
37+
struct B {}
38+
"""
39+
40+
let (concurrentEdits, originalSource, editedSource) = getConcurrentEditsAndSources(source)
3741

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)
42+
let origTree = Parser.parse(source: originalSource)
4043
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)
44+
let transition = IncrementalParseTransition(previousTree: origTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodeCollector)
45+
let newTree = Parser.parse(source: editedSource, parseTransition: transition)
46+
XCTAssertEqual("\(newTree)", editedSource)
4447

4548
let origStructB = origTree.statements[1]
4649
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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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, _) = getConcurrentEditsAndSources(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: originalSource)
53+
54+
XCTAssertEqual(sourceAppliedEdits, expectedSource)
55+
}
56+
}

0 commit comments

Comments
 (0)