Skip to content

Commit 9e4dcd8

Browse files
committed
[Foundation] Coalesce duplicate keys in String-keyed NSDictionary and NSSet instances
NSString has a stricter concept of equality than Swift’s String, so it is possible to construct NSDictionary/NSSet instances that contain duplicate keys according to Swift’s definition of string equality. This change improves how we handle these cases by coalescing the duplicate keys (keeping a single value). rdar://problem/35995647
1 parent c23cc1e commit 9e4dcd8

File tree

4 files changed

+80
-39
lines changed

4 files changed

+80
-39
lines changed

stdlib/public/SDK/Foundation/NSDictionary.swift

+20-20
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ extension Dictionary : _ObjectiveCBridgeable {
8080
return
8181
}
8282

83+
if Key.self == String.self {
84+
// String and NSString have different concepts of equality, so
85+
// string-keyed NSDictionaries may generate key collisions when bridged
86+
// over to Swift. See rdar://problem/35995647
87+
var dict = Dictionary(minimumCapacity: d.count)
88+
d.enumerateKeysAndObjects({ (anyKey: Any, anyValue: Any, _) in
89+
let key = Swift._forceBridgeFromObjectiveC(
90+
anyKey as AnyObject, Key.self)
91+
let value = Swift._forceBridgeFromObjectiveC(
92+
anyValue as AnyObject, Value.self)
93+
// FIXME: Log a warning if `dict` already had a value for `key`
94+
dict.updateValue(value, forKey: key)
95+
})
96+
result = dict
97+
return
98+
}
99+
83100
// `Dictionary<Key, Value>` where either `Key` or `Value` is a value type
84101
// may not be backed by an NSDictionary.
85102
var builder = _DictionaryBuilder<Key, Value>(count: d.count)
@@ -115,26 +132,9 @@ extension Dictionary : _ObjectiveCBridgeable {
115132
// dictionary; map it to an empty dictionary.
116133
if _slowPath(d == nil) { return Dictionary() }
117134

118-
if let native = [Key : Value]._bridgeFromObjectiveCAdoptingNativeStorageOf(
119-
d! as AnyObject) {
120-
return native
121-
}
122-
123-
if _isBridgedVerbatimToObjectiveC(Key.self) &&
124-
_isBridgedVerbatimToObjectiveC(Value.self) {
125-
return [Key : Value](
126-
_cocoaDictionary: unsafeBitCast(d! as AnyObject, to: _NSDictionary.self))
127-
}
128-
129-
// `Dictionary<Key, Value>` where either `Key` or `Value` is a value type
130-
// may not be backed by an NSDictionary.
131-
var builder = _DictionaryBuilder<Key, Value>(count: d!.count)
132-
d!.enumerateKeysAndObjects({ (anyKey: Any, anyValue: Any, _) in
133-
builder.add(
134-
key: Swift._forceBridgeFromObjectiveC(anyKey as AnyObject, Key.self),
135-
value: Swift._forceBridgeFromObjectiveC(anyValue as AnyObject, Value.self))
136-
})
137-
return builder.take()
135+
var result: Dictionary? = nil
136+
_forceBridgeFromObjectiveC(d!, result: &result)
137+
return result!
138138
}
139139
}
140140

stdlib/public/SDK/Foundation/NSSet.swift

+18-19
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ extension Set : _ObjectiveCBridgeable {
7373
return
7474
}
7575

76+
if Element.self == String.self {
77+
// String and NSString have different concepts of equality, so
78+
// string-keyed NSSets may generate key collisions when bridged over to
79+
// Swift. See rdar://problem/35995647
80+
var set = Set(minimumCapacity: s.count)
81+
s.enumerateObjects({ (anyMember: Any, _) in
82+
let member = Swift._forceBridgeFromObjectiveC(
83+
anyMember as AnyObject, Element.self)
84+
// FIXME: Log a warning if `member` is already in the set.
85+
set.insert(member)
86+
})
87+
result = set
88+
return
89+
}
90+
7691
// `Set<Element>` where `Element` is a value type may not be backed by
7792
// an NSSet.
7893
var builder = _SetBuilder<Element>(count: s.count)
@@ -101,25 +116,9 @@ extension Set : _ObjectiveCBridgeable {
101116
// set; map it to an empty set.
102117
if _slowPath(s == nil) { return Set() }
103118

104-
if let native =
105-
Set<Element>._bridgeFromObjectiveCAdoptingNativeStorageOf(s! as AnyObject) {
106-
107-
return native
108-
}
109-
110-
if _isBridgedVerbatimToObjectiveC(Element.self) {
111-
return Set<Element>(_cocoaSet: unsafeBitCast(s! as AnyObject,
112-
to: _NSSet.self))
113-
}
114-
115-
// `Set<Element>` where `Element` is a value type may not be backed by
116-
// an NSSet.
117-
var builder = _SetBuilder<Element>(count: s!.count)
118-
s!.enumerateObjects({ (anyMember: Any, _) in
119-
builder.add(member: Swift._forceBridgeFromObjectiveC(
120-
anyMember as AnyObject, Element.self))
121-
})
122-
return builder.take()
119+
var result: Set? = nil
120+
Set<Element>._forceBridgeFromObjectiveC(s!, result: &result)
121+
return result!
123122
}
124123
}
125124

validation-test/stdlib/Dictionary.swift

+20
Original file line numberDiff line numberDiff line change
@@ -3123,6 +3123,26 @@ DictionaryTestSuite.test("BridgedFromObjC.Nonverbatim.ArrayOfDictionaries") {
31233123
}
31243124
}
31253125

3126+
DictionaryTestSuite.test("BridgedFromObjC.Nonverbatim.StringEqualityMismatch") {
3127+
// NSString's isEqual(_:) implementation is stricter than Swift's String, so
3128+
// Dictionary values bridged over from Objective-C may have duplicate keys.
3129+
// rdar://problem/35995647
3130+
let cafe1 = "Cafe\u{301}" as NSString
3131+
let cafe2 = "Café" as NSString
3132+
3133+
let nsd = NSMutableDictionary()
3134+
nsd.setObject(42, forKey: cafe1)
3135+
nsd.setObject(23, forKey: cafe2)
3136+
expectEqual(2, nsd.count)
3137+
expectTrue((42 as NSNumber).isEqual(to: nsd.object(forKey: cafe1)))
3138+
expectTrue((23 as NSNumber).isEqual(to: nsd.object(forKey: cafe2)))
3139+
3140+
let d = convertNSDictionaryToDictionary(nsd) as [String: Int]
3141+
expectEqual(1, d.count)
3142+
expectEqual(d["Cafe\u{301}"], d["Café"])
3143+
let v = d["Café"]
3144+
expectTrue(v == 42 || v == 23)
3145+
}
31263146

31273147
//===---
31283148
// Dictionary -> NSDictionary bridging tests.

validation-test/stdlib/Set.swift

+22
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,28 @@ SetTestSuite.test("BridgedFromObjC.Nonverbatim.ArrayOfSets") {
21462146
}
21472147
}
21482148

2149+
SetTestSuite.test("BridgedFromObjC.Nonverbatim.StringEqualityMismatch") {
2150+
// NSString's isEqual(_:) implementation is stricter than Swift's String, so
2151+
// Set values bridged over from Objective-C may have duplicate keys.
2152+
// rdar://problem/35995647
2153+
let cafe1 = "Cafe\u{301}" as NSString
2154+
let cafe2 = "Café" as NSString
2155+
2156+
let nsset = NSMutableSet()
2157+
nsset.add(cafe1)
2158+
expectTrue(nsset.contains(cafe1))
2159+
expectFalse(nsset.contains(cafe2))
2160+
nsset.add(cafe2)
2161+
expectEqual(2, nsset.count)
2162+
expectTrue(nsset.contains(cafe1))
2163+
expectTrue(nsset.contains(cafe2))
2164+
2165+
let s: Set<String> = convertNSSetToSet(nsset)
2166+
expectEqual(1, s.count)
2167+
expectTrue(s.contains("Cafe\u{301}"))
2168+
expectTrue(s.contains("Café"))
2169+
expectTrue(Array(s) == ["Café"])
2170+
}
21492171

21502172
//===---
21512173
// Dictionary -> NSDictionary bridging tests.

0 commit comments

Comments
 (0)