From f73d906f18d2ba7f653e964739ed9cd9a4edc0f9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 12 Oct 2016 07:56:05 -0700 Subject: [PATCH] Use a single ShimMap class, and indicate that iteration always yields string keys (which it did before too) --- src/compiler/collections.ts | 155 +++++++++++++++--------------------- src/compiler/types.ts | 6 +- 2 files changed, 71 insertions(+), 90 deletions(-) diff --git a/src/compiler/collections.ts b/src/compiler/collections.ts index 40ae40bad05..f7b23fe77c3 100644 --- a/src/compiler/collections.ts +++ b/src/compiler/collections.ts @@ -2,7 +2,7 @@ /* @internal */ namespace ts { // The global Map object. This may not be available, so we must test for it. - declare const Map: NumberMapConstructor & StringMapConstructor | undefined; + declare const Map: { new(pairs?: [K, V][]): Map } | undefined; const usingNativeMaps = typeof Map !== "undefined"; // tslint:disable-next-line:no-in-operator const fullyFeaturedMaps = usingNativeMaps && "keys" in Map.prototype && "values" in Map.prototype && "entries" in Map.prototype; @@ -19,99 +19,63 @@ namespace ts { next(): { value: T, done: false } | { value: never, done: true }; } - export interface NumberMapConstructor { - /** - * Creates a new Map with number keys. - * If `pairs` is provided, each [key, value] pair will be added to the map. - */ - new(pairs?: [K, V][]): Map; - } - /** - * In runtimes without Maps, this is implemented using a sparse array. - * This is generic over the key type because it is usually an enum. + * Provides Map-like functionality for ES5 runtimes. + * This is intentionally *not* a full Map shim, and doesn't provide iterators (which aren't available for IE Maps anyway). + * We can only efficiently support strings and number keys, and iteration will always yield stringified keys. */ - export const NumberMap: NumberMapConstructor = usingNativeMaps ? Map : class ShimNumberMap implements Map { - private data: { [key: number]: V } = []; + class ShimMap implements Map { + private data = createDictionaryModeObject(); + + /* + So long as `K extends string | number`, we can cast `key as string` and insert it into the map. + However, `forEach` will iterate over strings because values are stringified before being put in the map. + */ constructor(pairs?: [K, V][]) { if (pairs) { for (const [key, value] of pairs) { - this.data[key as number] = value; + this.data[key as string] = value; } } } - clear() { - this.data = []; + clear(): void { + this.data = createDictionaryModeObject(); } - delete(key: K) { - delete this.data[key as number]; + delete(key: K): void { + delete this.data[key as string]; } - get(key: K) { - return this.data[key as number]; + get(key: K): V { + return this.data[key as string]; } - has(key: K) { + has(key: K): boolean { // tslint:disable-next-line:no-in-operator - return (key as number) in this.data; + return (key as string) in this.data; } - set(key: K, value: V) { - this.data[key as number] = value; + set(key: K, value: V): void { + this.data[key as string] = value; } - forEach(action: (value: V, key: K) => void) { + forEach(action: (value: V, key: string) => void): void { for (const key in this.data) { - action(this.data[key], key as any as K); + action(this.data[key], key); } } - }; - - export interface StringMapConstructor { - new(pairs?: [string, T][]): Map; } - /** In runtimes without Maps, this is implemented using an object. */ - export const StringMap: StringMapConstructor = usingNativeMaps ? Map : class ShimStringMap implements Map { - private data = createDictionaryModeObject(); - constructor(pairs?: [string, T][]) { - if (pairs) { - for (const [key, value] of pairs) { - this.data[key] = value; - } - } - } + /** + * In runtimes without Maps, this is implemented using an object. + * `pairs` is an optional list of entries to add to the new map. + */ + export const StringMap: { new(pairs?: [string, T][]): Map; } = usingNativeMaps ? Map : ShimMap; - clear() { - this.data = createDictionaryModeObject(); - } - - delete(key: string) { - delete this.data[key]; - } - - get(key: string) { - return this.data[key]; - } - - has(key: string) { - // tslint:disable-next-line:no-in-operator - return key in this.data; - } - - set(key: string, value: T) { - this.data[key] = value; - } - - forEach(f: (value: T, key: string) => void) { - for (const key in this.data) { - f(this.data[key], key); - } - } - }; + /** This is generic over the key type because it is usually an enum. */ + export const NumberMap: { new(pairs?: [K, V][]): Map } = usingNativeMaps ? Map : ShimMap; const createObject = Object.create; function createDictionaryModeObject(): MapLike { @@ -126,9 +90,12 @@ namespace ts { return map; } - /** Iterates over entries in the map, returning the first output of `getResult` that is not `undefined`. */ - export const findInMap: (map: Map, getResult: (value: V, key: K) => U | undefined) => U | undefined = fullyFeaturedMaps - ? (map: ES6Map, f: (value: V, key: K) => U | undefined) => { + /** + * Iterates over entries in the map, returning the first output of `getResult` that is not `undefined`. + * Only works for strings because shims iterate with `for-in`. + */ + export const findInMap: (map: Map, getResult: (value: V, key: string) => U | undefined) => U | undefined = fullyFeaturedMaps + ? (map: ES6Map, f: (value: V, key: string) => U | undefined) => { const iter = map.entries(); while (true) { const { value: pair, done } = iter.next(); @@ -142,7 +109,7 @@ namespace ts { } } } - : (map: Map, f: (value: V, key: K) => U | undefined) => { + : (map: Map, f: (value: V, key: string) => U | undefined) => { let result: U | undefined; map.forEach((value, key) => { if (result === undefined) @@ -151,11 +118,14 @@ namespace ts { return result; }; - /** Whether `predicate` is true for at least one entry in the map. */ - export const someInMap: (map: Map, predicate: (value: V, key: K) => boolean) => boolean = fullyFeaturedMaps - ? (map: ES6Map, predicate: (value: V, key: K) => boolean) => + /** + * Whether `predicate` is true for at least one entry in the map. + * Only works for strings because shims iterate with `for-in`. + */ + export const someInMap: (map: Map, predicate: (value: V, key: string) => boolean) => boolean = fullyFeaturedMaps + ? (map: ES6Map, predicate: (value: V, key: string) => boolean) => someInIterator(map.entries(), ([key, value]) => predicate(value, key)) - : (map: Map, predicate: (value: V, key: K) => boolean) => { + : (map: Map, predicate: (value: V, key: string) => boolean) => { let found = false; map.forEach((value, key) => { found = found || predicate(value, key); @@ -163,10 +133,13 @@ namespace ts { return found; }; - /** Whether `predicate` is true for at least one key in the map. */ - export const someKeyInMap: (map: Map, predicate: (key: K) => boolean) => boolean = fullyFeaturedMaps - ? (map: ES6Map, predicate: (key: K) => boolean) => someInIterator(map.keys(), predicate) - : (map: Map, predicate: (key: K) => boolean) => + /** + * Whether `predicate` is true for at least one key in the map. + * Only works for strings because shims iterate with `for-in`. + */ + export const someKeyInMap: (map: Map, predicate: (key: string) => boolean) => boolean = fullyFeaturedMaps + ? (map: ES6Map, predicate: (key: string) => boolean) => someInIterator(map.keys(), predicate) + : (map: Map, predicate: (key: string) => boolean) => someInMap(map, (_value, key) => predicate(key)); /** Whether `predicate` is true for at least one value in the map. */ @@ -190,19 +163,20 @@ namespace ts { /** * Equivalent to the ES6 code: * `for (const key of map.keys()) action(key);` + * Only works for strings because shims iterate with `for-in`. */ - export const forEachKeyInMap: (map: Map, action: (key: K) => void) => void = fullyFeaturedMaps - ? (map: ES6Map, f: (key: K) => void) => { - const iter: Iterator = map.keys(); + export const forEachKeyInMap: (map: Map, action: (key: string) => void) => void = fullyFeaturedMaps + ? (map: ES6Map, action: (key: string) => void) => { + const iter: Iterator = map.keys(); while (true) { const { value: key, done } = iter.next(); if (done) { return; } - f(key); + action(key); } } - : (map: Map, action: (key: K) => void) => { + : (map: Map, action: (key: string) => void) => { map.forEach((_value, key) => action(key)); }; @@ -310,14 +284,17 @@ namespace ts { * @param target A map to which properties should be copied. */ export function copyMapEntriesFromTo(source: Map, target: Map): void { - source.forEach((value, key) => { + source.forEach((value: V, key: K) => { target.set(key, value); }); } - /** Equivalent to `Array.from(map.keys())`. */ - export function keysOfMap(map: Map): K[] { - const keys: K[] = []; + /** + * Equivalent to `Array.from(map.keys())`. + * Only works for strings because shims iterate with `for-in`. + */ + export function keysOfMap(map: Map): string[] { + const keys: string[] = []; forEachKeyInMap(map, key => { keys.push(key); }); return keys; } @@ -442,7 +419,7 @@ namespace ts { } /** True if the maps have the same keys and values. */ - export function mapsAreEqual(left: Map, right: Map, valuesAreEqual?: (left: V, right: V) => boolean): boolean { + export function mapsAreEqual(left: Map, right: Map, valuesAreEqual?: (left: V, right: V) => boolean): boolean { if (left === right) return true; if (!left || !right) return false; const someInLeftHasNoMatch = someInMap(left, (leftValue, leftKey) => { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 69061d42d6d..0e7f2fbc213 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -18,7 +18,11 @@ namespace ts { export interface Map { clear(): void; delete(key: K): void; - forEach(action: (value: V, key: K) => void): void; + /** + * Call `action` for each entry in the map. + * Since we use a `for-in` loop for our shims, `key` may be a string. + */ + forEach(action: (value: V, key: K | string) => void): void; get(key: K): V; /** * Whether the key is in the map.