Skip to content

Commit 3e4a9d8

Browse files
author
Max Moiseev
committed
[overlay] Fix Foundation extensions to Substring
When a substring gets bridged to NSString, it loses the initial offset, therefore APIs that accept or return StringIndex ranges should handle this case explicitly by adding/subtracting the substring start offset. Fixes <rdar://problem/33873277> (cherry picked from commit 7322b63)
1 parent 84b5616 commit 3e4a9d8

File tree

3 files changed

+187
-28
lines changed

3 files changed

+187
-28
lines changed

stdlib/public/SDK/Foundation/NSStringAPI.swift

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ func _toNSArray<T, U : AnyObject>(_ a: [T], f: (T) -> U) -> NSArray {
3030
return result
3131
}
3232

33-
func _toNSRange(_ r: Range<String.Index>) -> NSRange {
34-
return NSRange(
35-
location: r.lowerBound.encodedOffset,
36-
length: r.upperBound.encodedOffset - r.lowerBound.encodedOffset)
37-
}
38-
3933
// We only need this for UnsafeMutablePointer, but there's not currently a way
4034
// to write that constraint.
4135
extension Optional {
@@ -417,10 +411,26 @@ extension StringProtocol where Index == String.Index {
417411
return self._ephemeralString as NSString
418412
}
419413

414+
// self can be a Substring so we need to subtract/add this offset when
415+
// passing _ns to the Foundation APIs. Will be 0 if self is String.
416+
@_inlineable
417+
@_versioned
418+
internal var _substringOffset: Int {
419+
return self.startIndex.encodedOffset
420+
}
421+
420422
/// Return an `Index` corresponding to the given offset in our UTF-16
421423
/// representation.
422424
func _index(_ utf16Index: Int) -> Index {
423-
return Index(encodedOffset: utf16Index)
425+
return Index(encodedOffset: utf16Index + _substringOffset)
426+
}
427+
428+
@_inlineable
429+
@_versioned
430+
internal func _toRelativeNSRange(_ r: Range<String.Index>) -> NSRange {
431+
return NSRange(
432+
location: r.lowerBound.encodedOffset - _substringOffset,
433+
length: r.upperBound.encodedOffset - r.lowerBound.encodedOffset)
424434
}
425435

426436
/// Return a `Range<Index>` corresponding to the given `NSRange` of
@@ -581,7 +591,7 @@ extension StringProtocol where Index == String.Index {
581591
return locale != nil ? _ns.compare(
582592
aString,
583593
options: mask,
584-
range: _toNSRange(
594+
range: _toRelativeNSRange(
585595
range ?? startIndex..<endIndex
586596
),
587597
locale: locale
@@ -590,7 +600,7 @@ extension StringProtocol where Index == String.Index {
590600
: range != nil ? _ns.compare(
591601
aString,
592602
options: mask,
593-
range: _toNSRange(range!)
603+
range: _toRelativeNSRange(range!)
594604
)
595605

596606
: !mask.isEmpty ? _ns.compare(aString, options: mask)
@@ -1008,7 +1018,7 @@ extension StringProtocol where Index == String.Index {
10081018
T : StringProtocol, R : RangeExpression
10091019
>(in range: R, with replacement: T) -> String where R.Bound == Index {
10101020
return _ns.replacingCharacters(
1011-
in: _toNSRange(range.relative(to: self)),
1021+
in: _toRelativeNSRange(range.relative(to: self)),
10121022
with: replacement._ephemeralString)
10131023
}
10141024

@@ -1041,7 +1051,7 @@ extension StringProtocol where Index == String.Index {
10411051
of: target,
10421052
with: replacement,
10431053
options: options,
1044-
range: _toNSRange(
1054+
range: _toRelativeNSRange(
10451055
searchRange ?? startIndex..<endIndex
10461056
)
10471057
)
@@ -1163,7 +1173,7 @@ extension StringProtocol where Index == String.Index {
11631173
) where R.Bound == Index {
11641174
let range = range.relative(to: self)
11651175
_ns.enumerateLinguisticTags(
1166-
in: _toNSRange(range),
1176+
in: _toRelativeNSRange(range),
11671177
scheme: tagScheme._ephemeralString,
11681178
options: opts,
11691179
orthography: orthography != nil ? orthography! : nil
@@ -1227,7 +1237,7 @@ extension StringProtocol where Index == String.Index {
12271237
) -> Void
12281238
) where R.Bound == Index {
12291239
_ns.enumerateSubstrings(
1230-
in: _toNSRange(range.relative(to: self)), options: opts) {
1240+
in: _toRelativeNSRange(range.relative(to: self)), options: opts) {
12311241
var stop_ = false
12321242

12331243
body($0,
@@ -1300,7 +1310,7 @@ extension StringProtocol where Index == String.Index {
13001310
usedLength: usedBufferCount,
13011311
encoding: encoding.rawValue,
13021312
options: options,
1303-
range: _toNSRange(range.relative(to: self)),
1313+
range: _toRelativeNSRange(range.relative(to: self)),
13041314
remaining: $0)
13051315
}
13061316
}
@@ -1327,7 +1337,7 @@ extension StringProtocol where Index == String.Index {
13271337
contentsEnd in self._ns.getLineStart(
13281338
start, end: end,
13291339
contentsEnd: contentsEnd,
1330-
for: _toNSRange(range.relative(to: self)))
1340+
for: _toRelativeNSRange(range.relative(to: self)))
13311341
}
13321342
}
13331343
}
@@ -1355,7 +1365,7 @@ extension StringProtocol where Index == String.Index {
13551365
contentsEnd in self._ns.getParagraphStart(
13561366
start, end: end,
13571367
contentsEnd: contentsEnd,
1358-
for: _toNSRange(range.relative(to: self)))
1368+
for: _toRelativeNSRange(range.relative(to: self)))
13591369
}
13601370
}
13611371
}
@@ -1382,7 +1392,8 @@ extension StringProtocol where Index == String.Index {
13821392
public func lineRange<
13831393
R : RangeExpression
13841394
>(for aRange: R) -> Range<Index> where R.Bound == Index {
1385-
return _range(_ns.lineRange(for: _toNSRange(aRange.relative(to: self))))
1395+
return _range(_ns.lineRange(
1396+
for: _toRelativeNSRange(aRange.relative(to: self))))
13861397
}
13871398

13881399
// - (NSArray *)
@@ -1406,7 +1417,7 @@ extension StringProtocol where Index == String.Index {
14061417
var nsTokenRanges: NSArray?
14071418
let result = tokenRanges._withNilOrAddress(of: &nsTokenRanges) {
14081419
self._ns.linguisticTags(
1409-
in: _toNSRange(range.relative(to: self)),
1420+
in: _toRelativeNSRange(range.relative(to: self)),
14101421
scheme: tagScheme._ephemeralString,
14111422
options: opts,
14121423
orthography: orthography,
@@ -1430,7 +1441,7 @@ extension StringProtocol where Index == String.Index {
14301441
R : RangeExpression
14311442
>(for aRange: R) -> Range<Index> where R.Bound == Index {
14321443
return _range(
1433-
_ns.paragraphRange(for: _toNSRange(aRange.relative(to: self))))
1444+
_ns.paragraphRange(for: _toRelativeNSRange(aRange.relative(to: self))))
14341445
}
14351446

14361447
// - (NSRange)rangeOfCharacterFromSet:(NSCharacterSet *)aSet
@@ -1456,7 +1467,7 @@ extension StringProtocol where Index == String.Index {
14561467
_ns.rangeOfCharacter(
14571468
from: aSet,
14581469
options: mask,
1459-
range: _toNSRange(
1470+
range: _toRelativeNSRange(
14601471
aRange ?? startIndex..<endIndex
14611472
)
14621473
)
@@ -1487,7 +1498,7 @@ extension StringProtocol where Index == String.Index {
14871498
// and output ranges due (if nothing else) to locale changes
14881499
return _range(
14891500
_ns.rangeOfComposedCharacterSequences(
1490-
for: _toNSRange(range.relative(to: self))))
1501+
for: _toRelativeNSRange(range.relative(to: self))))
14911502
}
14921503

14931504
// - (NSRange)rangeOfString:(NSString *)aString
@@ -1522,13 +1533,13 @@ extension StringProtocol where Index == String.Index {
15221533
locale != nil ? _ns.range(
15231534
of: aString,
15241535
options: mask,
1525-
range: _toNSRange(
1536+
range: _toRelativeNSRange(
15261537
searchRange ?? startIndex..<endIndex
15271538
),
15281539
locale: locale
15291540
)
15301541
: searchRange != nil ? _ns.range(
1531-
of: aString, options: mask, range: _toNSRange(searchRange!)
1542+
of: aString, options: mask, range: _toRelativeNSRange(searchRange!)
15321543
)
15331544
: !mask.isEmpty ? _ns.range(of: aString, options: mask)
15341545
: _ns.range(of: aString)
@@ -1637,7 +1648,7 @@ extension StringProtocol where Index == String.Index {
16371648
@available(swift, deprecated: 4.0,
16381649
message: "Please use String slicing subscript.")
16391650
public func substring(with aRange: Range<Index>) -> String {
1640-
return _ns.substring(with: _toNSRange(aRange))
1651+
return _ns.substring(with: _toRelativeNSRange(aRange))
16411652
}
16421653
}
16431654

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// RUN: rm -rf %t ; mkdir -p %t
2+
// RUN: %target-build-swift %s -o %t/a.out4 -swift-version 4 && %target-run %t/a.out4
3+
// REQUIRES: executable_test
4+
5+
// REQUIRES: objc_interop
6+
7+
//
8+
// Tests for the NSString APIs on Substring
9+
//
10+
11+
import StdlibUnittest
12+
13+
import Foundation
14+
15+
16+
extension String {
17+
func range(fromStart: Int, fromEnd: Int) -> Range<String.Index> {
18+
return index(startIndex, offsetBy: fromStart) ..<
19+
index(endIndex, offsetBy: fromEnd)
20+
}
21+
subscript(fromStart: Int, fromEnd: Int) -> SubSequence {
22+
return self[range(fromStart: fromStart, fromEnd: fromEnd)]
23+
}
24+
}
25+
26+
var tests = TestSuite("NSStringAPIs/Substring")
27+
28+
tests.test("range(of:)/NilRange") {
29+
let ss = "aabcdd"[1, -1]
30+
let range = ss.range(of: "bc")
31+
expectOptionalEqual("bc", range.map { ss[$0] })
32+
}
33+
34+
tests.test("range(of:)/NonNilRange") {
35+
let s = "aabcdd"
36+
let ss = s[1, -1]
37+
let searchRange = s.range(fromStart: 2, fromEnd: -2)
38+
let range = ss.range(of: "bc", range: searchRange)
39+
expectOptionalEqual("bc", range.map { ss[$0] })
40+
}
41+
42+
tests.test("rangeOfCharacter") {
43+
let ss = "__hello__"[2, -2]
44+
let range = ss.rangeOfCharacter(from: CharacterSet.alphanumerics)
45+
expectOptionalEqual("h", range.map { ss[$0] })
46+
}
47+
48+
tests.test("compare(_:options:range:locale:)/NilRange") {
49+
let needle = "hello"
50+
let haystack = "__hello__"[2, -2]
51+
expectEqual(.orderedSame, haystack.compare(needle))
52+
}
53+
54+
tests.test("compare(_:options:range:locale:)/NonNilRange") {
55+
let needle = "hello"
56+
let haystack = "__hello__"
57+
let range = haystack.range(fromStart: 2, fromEnd: -2)
58+
expectEqual(.orderedSame, haystack[range].compare(needle, range: range))
59+
}
60+
61+
tests.test("replacingCharacters(in:with:)") {
62+
let s = "__hello, world"
63+
let range = s.range(fromStart: 2, fromEnd: -7)
64+
let expected = "__goodbye, world"
65+
let replacement = "goodbye"
66+
expectEqual(expected,
67+
s.replacingCharacters(in: range, with: replacement))
68+
expectEqual(expected[2, 0],
69+
s[2, 0].replacingCharacters(in: range, with: replacement))
70+
71+
expectEqual(replacement,
72+
s.replacingCharacters(in: s.startIndex..., with: replacement))
73+
expectEqual(replacement,
74+
s.replacingCharacters(in: ..<s.endIndex, with: replacement))
75+
expectEqual(expected[2, 0],
76+
s[2, 0].replacingCharacters(in: range, with: replacement[...]))
77+
}
78+
79+
tests.test("replacingOccurrences(of:with:options:range:)/NilRange") {
80+
let s = "hello"
81+
82+
expectEqual("he11o", s.replacingOccurrences(of: "l", with: "1"))
83+
expectEqual("he11o", s.replacingOccurrences(of: "l"[...], with: "1"))
84+
expectEqual("he11o", s.replacingOccurrences(of: "l", with: "1"[...]))
85+
expectEqual("he11o", s.replacingOccurrences(of: "l"[...], with: "1"[...]))
86+
87+
expectEqual("he11o",
88+
s[...].replacingOccurrences(of: "l", with: "1"))
89+
expectEqual("he11o",
90+
s[...].replacingOccurrences(of: "l"[...], with: "1"))
91+
expectEqual("he11o",
92+
s[...].replacingOccurrences(of: "l", with: "1"[...]))
93+
expectEqual("he11o",
94+
s[...].replacingOccurrences(of: "l"[...], with: "1"[...]))
95+
}
96+
97+
tests.test("replacingOccurrences(of:with:options:range:)/NonNilRange") {
98+
let s = "hello"
99+
let r = s.range(fromStart: 1, fromEnd: -2)
100+
101+
expectEqual("he1lo",
102+
s.replacingOccurrences(of: "l", with: "1", range: r))
103+
expectEqual("he1lo",
104+
s.replacingOccurrences(of: "l"[...], with: "1", range: r))
105+
expectEqual("he1lo",
106+
s.replacingOccurrences(of: "l", with: "1"[...], range: r))
107+
expectEqual("he1lo",
108+
s.replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
109+
110+
expectEqual("he1lo",
111+
s[...].replacingOccurrences(of: "l", with: "1", range: r))
112+
expectEqual("he1lo",
113+
s[...].replacingOccurrences(of: "l"[...], with: "1", range: r))
114+
expectEqual("he1lo",
115+
s[...].replacingOccurrences(of: "l", with: "1"[...], range: r))
116+
expectEqual("he1lo",
117+
s[...].replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
118+
119+
let ss = s[1, -1]
120+
expectEqual("e1l",
121+
ss.replacingOccurrences(of: "l", with: "1", range: r))
122+
expectEqual("e1l",
123+
ss.replacingOccurrences(of: "l"[...], with: "1", range: r))
124+
expectEqual("e1l",
125+
ss.replacingOccurrences(of: "l", with: "1"[...], range: r))
126+
expectEqual("e1l",
127+
ss.replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
128+
}
129+
130+
tests.test("substring(with:)") {
131+
let s = "hello, world"
132+
let r = s.range(fromStart: 7, fromEnd: 0)
133+
expectEqual("world", s.substring(with: r))
134+
expectEqual("world", s[...].substring(with: r))
135+
expectEqual("world", s[1, 0].substring(with: r))
136+
}
137+
138+
tests.test("substring(with:)/SubscriptEquivalence") {
139+
let s = "hello, world"
140+
let r = s.range(fromStart: 7, fromEnd: 0)
141+
expectEqual(s[r], s.substring(with: r))
142+
expectEqual(s[...][r], s[...].substring(with: r))
143+
expectEqual(s[1, 0][r], s[1, 0].substring(with: r))
144+
}
145+
146+
runAllTests()

test/stdlib/NSStringAPI.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift -swift-version 3
1+
// RUN: %target-run-simple-swift
22
// REQUIRES: executable_test
33

44
// REQUIRES: objc_interop
@@ -1144,9 +1144,11 @@ NSStringAPIs.test("rangeOfComposedCharacterSequences(for:)") {
11441144
for: s.index(s.startIndex, offsetBy: 8)..<s.index(s.startIndex, offsetBy: 10))])
11451145
}
11461146

1147-
func toIntRange(
1148-
_ string: String, _ maybeRange: Range<String.Index>?
1149-
) -> Range<Int>? {
1147+
func toIntRange<
1148+
S : StringProtocol
1149+
>(
1150+
_ string: S, _ maybeRange: Range<String.Index>?
1151+
) -> Range<Int>? where S.Index == String.Index, S.IndexDistance == Int {
11501152
guard let range = maybeRange else { return nil }
11511153

11521154
return

0 commit comments

Comments
 (0)