Skip to content

Commit 374160e

Browse files
committed
Add helper function for getting the ConcurrentEdits of two strings
1 parent 8311d8d commit 374160e

File tree

4 files changed

+157
-47
lines changed

4 files changed

+157
-47
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
/// Returns the `ConcurrentEdits`s to transition from `base` to `new`.
17+
public func getConcurrentEdits(from base: String, to new: String) -> ConcurrentEdits {
18+
let diffCollection = new.difference(from: base)
19+
20+
let diffArray: [(offset: Int, isInsert: Bool)] =
21+
diffCollection
22+
.map { diff in
23+
switch diff {
24+
case .remove(offset: let offset, element: _, associatedWith: _):
25+
return (offset: offset, isInsert: false)
26+
case .insert(offset: let offset, element: _, associatedWith: _):
27+
return (offset: offset, isInsert: true)
28+
}
29+
}
30+
.sorted {
31+
// Change.remove should prior to Change.insert
32+
if $0.offset < $1.offset {
33+
return true
34+
} else if $0.offset == $1.offset {
35+
return !$0.isInsert
36+
} else {
37+
return false
38+
}
39+
}
40+
41+
let sourceEdits = diffArray.map({
42+
return $0.isInsert ? SourceEdit(offset: $0.offset, length: 0, replacementLength: 1) : SourceEdit(offset: $0.offset, length: 1, replacementLength: 0)
43+
})
44+
45+
return ConcurrentEdits(fromSequential: sourceEdits)
46+
}
47+
48+
/// Apply the given edits to `testString` and return the resulting string.
49+
/// `concurrent` specifies whether the edits should be interpreted as being
50+
/// applied sequentially or concurrently.
51+
public func applyEdits(
52+
_ edits: [SourceEdit],
53+
concurrent: Bool,
54+
to testString: String,
55+
replacementChar: Character = "?"
56+
) -> String {
57+
guard let replacementAscii = replacementChar.asciiValue else {
58+
fatalError("replacementChar must be an ASCII character")
59+
}
60+
var edits = edits
61+
if concurrent {
62+
XCTAssert(ConcurrentEdits._isValidConcurrentEditArray(edits))
63+
64+
// If the edits are concurrent, sorted and not overlapping (as guaranteed by
65+
// the check above, we can apply them sequentially to the string in reverse
66+
// order because later edits don't affect earlier edits.
67+
edits = edits.reversed()
68+
}
69+
var bytes = Array(testString.utf8)
70+
for edit in edits {
71+
assert(edit.endOffset <= bytes.count)
72+
bytes.removeSubrange(edit.offset..<edit.endOffset)
73+
bytes.insert(contentsOf: [UInt8](repeating: replacementAscii, count: edit.replacementLength), at: edit.offset)
74+
}
75+
return String(bytes: bytes, encoding: .utf8)!
76+
}

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 25 additions & 16 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 {
18-
1919
public func testIncrementalInvalid() {
2020
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 newSource = "struct AA { func f() {"
22+
23+
let concurrentEdits = getConcurrentEdits(from: original, to: newSource)
24+
25+
let oldTree = Parser.parse(source: original)
26+
let lookup = IncrementalParseTransition(previousTree: oldTree, edits: concurrentEdits)
27+
let newTree = Parser.parse(source: newSource, parseTransition: lookup)
28+
29+
XCTAssertEqual("\(newTree)", newSource)
2930
}
3031

3132
public func testReusedNode() throws {
3233
try XCTSkipIf(true, "Swift parser does not handle node reuse yet")
3334

34-
let original = "struct A {}\nstruct B {}\n"
35-
let step: (String, (Int, Int, String)) =
36-
("struct AA {}\nstruct B {}\n", (8, 0, "A"))
35+
let original =
36+
"""
37+
struct A {}
38+
struct B {}
39+
"""
40+
41+
let newSource =
42+
"""
43+
struct A {}
44+
struct B {}
45+
"""
3746

3847
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 concurrentEdits = getConcurrentEdits(from: original, to: newSource)
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: newSource, parseTransition: transition)
52+
XCTAssertEqual("\(newTree)", newSource)
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 testDiffOfTwoStringsSimple() throws {
19+
let s1 = "struct A { func f() {"
20+
let s2 = "struct AA { func f() {"
21+
22+
let diffs = getConcurrentEdits(from: s1, to: s2)
23+
XCTAssertEqual(diffs.edits.count, 1)
24+
25+
let firstDiff = try XCTUnwrap(diffs.edits.first)
26+
XCTAssertEqual(firstDiff, SourceEdit(offset: 8, length: 0, replacementLength: 1))
27+
}
28+
29+
public func testDiffOfTwoSameStrings() {
30+
let s1 = "0123456"
31+
32+
let diffs = getConcurrentEdits(from: s1, to: s1)
33+
XCTAssert(diffs.edits.isEmpty)
34+
}
35+
36+
public func testDiffOfTwoStrings() {
37+
let s1 = "0123456"
38+
let s2 = "x12456yz"
39+
40+
let diffs = getConcurrentEdits(from: s1, to: s2)
41+
42+
let expectedDiffs: [SourceEdit] = [
43+
SourceEdit(offset: 0, length: 1, replacementLength: 1),
44+
SourceEdit(offset: 3, length: 1, replacementLength: 0),
45+
SourceEdit(offset: 7, length: 0, replacementLength: 2),
46+
]
47+
48+
XCTAssertEqual(diffs.edits, expectedDiffs)
49+
50+
let s3 = applyEdits(expectedDiffs, concurrent: true, to: s1)
51+
52+
XCTAssertEqual(s3, "?12456??")
53+
}
54+
}

0 commit comments

Comments
 (0)