Skip to content

Commit 547ab8c

Browse files
authored
Merge pull request #1684 from StevenWong12/use_marker_to_mark_source_edits
Use marker to mark `SourceEdit`s
2 parents 3d059c4 + b605766 commit 547ab8c

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)