Skip to content
This repository was archived by the owner on Jul 23, 2021. It is now read-only.

Commit 45196e3

Browse files
authored
Hash symbols as objects (#17)
1 parent d2575be commit 45196e3

File tree

3 files changed

+93
-27
lines changed

3 files changed

+93
-27
lines changed

__tests__/hash.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ describe('hash', () => {
3636
expect(hash(objA)).not.toBe(hash(objB));
3737
});
3838

39+
it('generates different hashes for different symbols', () => {
40+
const symA = Symbol();
41+
const symB = Symbol();
42+
expect(hash(symA)).toBe(hash(symA));
43+
expect(hash(symA)).not.toBe(hash(symB));
44+
});
45+
3946
it('generates different hashes for different functions', () => {
4047
const funA = () => {
4148
return;

__tests__/issues.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,27 @@ describe('Issue #1293', () => {
101101
expect(secondState).toEqual(firstState);
102102
});
103103
});
104+
105+
describe('Issue #1643', () => {
106+
[
107+
['a string', 'test'],
108+
['a number', 5],
109+
['null', null],
110+
['undefined', undefined],
111+
['a boolean', true],
112+
['an object', {}],
113+
['an array', []],
114+
['a function', () => null],
115+
].forEach(([label, value]) => {
116+
class MyClass {
117+
valueOf() {
118+
return value;
119+
}
120+
}
121+
122+
it(`Collection#hashCode() should handle objects that return ${label} for valueOf`, () => {
123+
const set = Set().add(new MyClass());
124+
set.hashCode();
125+
});
126+
});
127+
});

src/Hash.js

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,50 @@ import { smi } from './Math';
1010
const defaultValueOf = Object.prototype.valueOf;
1111

1212
export function hash(o) {
13-
switch (typeof o) {
13+
if (o == null) {
14+
return hashNullish(o);
15+
}
16+
17+
if (typeof o.hashCode === 'function') {
18+
// Drop any high bits from accidentally long hash codes.
19+
return smi(o.hashCode(o));
20+
}
21+
22+
const v = valueOf(o);
23+
24+
if (v == null) {
25+
return hashNullish(v);
26+
}
27+
28+
switch (typeof v) {
1429
case 'boolean':
1530
// The hash values for built-in constants are a 1 value for each 5-byte
1631
// shift region expect for the first, which encodes the value. This
1732
// reduces the odds of a hash collision for these common values.
18-
return o ? 0x42108421 : 0x42108420;
33+
return v ? 0x42108421 : 0x42108420;
1934
case 'number':
20-
return hashNumber(o);
35+
return hashNumber(v);
2136
case 'string':
22-
return o.length > STRING_HASH_CACHE_MIN_STRLEN
23-
? cachedHashString(o)
24-
: hashString(o);
37+
return v.length > STRING_HASH_CACHE_MIN_STRLEN
38+
? cachedHashString(v)
39+
: hashString(v);
2540
case 'object':
2641
case 'function':
27-
if (o === null) {
28-
return 0x42108422;
29-
}
30-
if (typeof o.hashCode === 'function') {
31-
// Drop any high bits from accidentally long hash codes.
32-
return smi(o.hashCode(o));
33-
}
34-
if (o.valueOf !== defaultValueOf && typeof o.valueOf === 'function') {
35-
o = o.valueOf(o);
36-
}
37-
return hashJSObj(o);
38-
case 'undefined':
39-
return 0x42108423;
42+
return hashJSObj(v);
43+
case 'symbol':
44+
return hashSymbol(v);
4045
default:
41-
if (typeof o.toString === 'function') {
42-
return hashString(o.toString());
46+
if (typeof v.toString === 'function') {
47+
return hashString(v.toString());
4348
}
44-
throw new Error('Value type ' + typeof o + ' cannot be hashed.');
49+
throw new Error('Value type ' + typeof v + ' cannot be hashed.');
4550
}
4651
}
4752

53+
function hashNullish(nullish) {
54+
return nullish === null ? 0x42108422 : /* undefined */ 0x42108423;
55+
}
56+
4857
// Compress arbitrarily large numbers into smi hashes.
4958
function hashNumber(n) {
5059
if (n !== n || n === Infinity) {
@@ -90,6 +99,19 @@ function hashString(string) {
9099
return smi(hashed);
91100
}
92101

102+
function hashSymbol(sym) {
103+
let hashed = symbolMap[sym];
104+
if (hashed !== undefined) {
105+
return hashed;
106+
}
107+
108+
hashed = nextHash();
109+
110+
symbolMap[sym] = hashed;
111+
112+
return hashed;
113+
}
114+
93115
function hashJSObj(obj) {
94116
let hashed;
95117
if (usingWeakMap) {
@@ -116,10 +138,7 @@ function hashJSObj(obj) {
116138
}
117139
}
118140

119-
hashed = ++objHashUID;
120-
if (objHashUID & 0x40000000) {
121-
objHashUID = 0;
122-
}
141+
hashed = nextHash();
123142

124143
if (usingWeakMap) {
125144
weakMap.set(obj, hashed);
@@ -186,14 +205,30 @@ function getIENodeHash(node) {
186205
}
187206
}
188207

208+
function valueOf(obj) {
209+
return obj.valueOf !== defaultValueOf && typeof obj.valueOf === 'function'
210+
? obj.valueOf(obj)
211+
: obj;
212+
}
213+
214+
function nextHash() {
215+
const nextHash = ++_objHashUID;
216+
if (_objHashUID & 0x40000000) {
217+
_objHashUID = 0;
218+
}
219+
return nextHash;
220+
}
221+
189222
// If possible, use a WeakMap.
190223
const usingWeakMap = typeof WeakMap === 'function';
191224
let weakMap;
192225
if (usingWeakMap) {
193226
weakMap = new WeakMap();
194227
}
195228

196-
let objHashUID = 0;
229+
const symbolMap = Object.create(null);
230+
231+
let _objHashUID = 0;
197232

198233
let UID_HASH_KEY = '__immutablehash__';
199234
if (typeof Symbol === 'function') {

0 commit comments

Comments
 (0)