dict: Assert that Dict is only used with string keys.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
Anders Kaseorg 2020-01-31 20:10:50 -08:00 committed by Tim Abbott
parent ad06810501
commit e4259d48a5
3 changed files with 43 additions and 24 deletions

View File

@ -434,6 +434,7 @@
"empty-returns/main": "off", "empty-returns/main": "off",
"indent": "off", "indent": "off",
"func-call-spacing": "off", "func-call-spacing": "off",
"no-extra-parens": "off",
"no-magic-numbers": "off", "no-magic-numbers": "off",
"semi": "off", "semi": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
@ -459,6 +460,7 @@
"@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-parens": ["error", "all"],
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "off", "@typescript-eslint/no-for-in-array": "off",
"@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-inferrable-types": "error",

View File

@ -40,6 +40,7 @@ run_test('basic', () => {
}); });
run_test('undefined_keys', () => { run_test('undefined_keys', () => {
blueslip.clear_test_data();
blueslip.set_test_data('error', 'Tried to call a Dict method with an undefined key.'); blueslip.set_test_data('error', 'Tried to call a Dict method with an undefined key.');
const d = new Dict(); const d = new Dict();
@ -49,6 +50,17 @@ run_test('undefined_keys', () => {
assert.equal(blueslip.get_test_logs('error').length, 2); assert.equal(blueslip.get_test_logs('error').length, 2);
}); });
run_test('non-strings', () => {
blueslip.clear_test_data();
blueslip.set_test_data('error', 'Tried to call a Dict method with a non-string.');
const d = new Dict();
d.set('17', 'value');
assert.equal(d.get(17), 'value');
assert.equal(blueslip.get_test_logs('error').length, 1);
});
run_test('restricted_keys', () => { run_test('restricted_keys', () => {
const d = new Dict(); const d = new Dict();

View File

@ -1,23 +1,23 @@
import * as _ from 'underscore'; import * as _ from 'underscore';
type KeyValue<K, V> = { k: K; v: V }; type KeyValue<V> = { k: string; v: V };
type Items<K, V> = { type Items<V> = {
[key: string]: KeyValue<K, V>; [key: string]: KeyValue<V>;
}; };
export class Dict<K, V> { export class Dict<V> {
private _items: Items<K, V> = {}; private _items: Items<V> = {};
/** /**
* Constructs a Dict object from an existing object's keys and values. * Constructs a Dict object from an existing object's keys and values.
* @param obj - A javascript object * @param obj - A javascript object
*/ */
static from<V>(obj: { [key: string]: V }): Dict<string, V> { static from<V>(obj: { [key: string]: V }): Dict<V> {
if (typeof obj !== "object" || obj === null) { if (typeof obj !== "object" || obj === null) {
throw new TypeError("Cannot convert argument to Dict"); throw new TypeError("Cannot convert argument to Dict");
} }
const dict = new Dict<string, V>(); const dict = new Dict<V>();
_.each(obj, function (val: V, key: string) { _.each(obj, function (val: V, key: string) {
dict.set(key, val); dict.set(key, val);
}); });
@ -30,25 +30,25 @@ export class Dict<K, V> {
* Intended for use as a set data structure. * Intended for use as a set data structure.
* @param arr - An array of keys * @param arr - An array of keys
*/ */
static from_array<K, V>(arr: K[]): Dict<K, V | true> { static from_array<V>(arr: string[]): Dict<V | true> {
if (!(arr instanceof Array)) { if (!(arr instanceof Array)) {
throw new TypeError("Argument is not an array"); throw new TypeError("Argument is not an array");
} }
const dict = new Dict<K, V | true>(); const dict = new Dict<V | true>();
for (const key of arr) { for (const key of arr) {
dict.set(key, true); dict.set(key, true);
} }
return dict; return dict;
} }
clone(): Dict<K, V> { clone(): Dict<V> {
const dict = new Dict<K, V>(); const dict = new Dict<V>();
dict._items = { ...this._items }; dict._items = { ...this._items };
return dict; return dict;
} }
get(key: K): V | undefined { get(key: string): V | undefined {
const mapping = this._items[this._munge(key)]; const mapping = this._items[this._munge(key)];
if (mapping === undefined) { if (mapping === undefined) {
return undefined; return undefined;
@ -56,7 +56,7 @@ export class Dict<K, V> {
return mapping.v; return mapping.v;
} }
set(key: K, value: V): V { set(key: string, value: V): V {
this._items[this._munge(key)] = {k: key, v: value}; this._items[this._munge(key)] = {k: key, v: value};
return value; return value;
} }
@ -65,7 +65,7 @@ export class Dict<K, V> {
* If `key` exists in the Dict, return its value. Otherwise insert `key` * If `key` exists in the Dict, return its value. Otherwise insert `key`
* with a value of `value` and return the value. * with a value of `value` and return the value.
*/ */
setdefault(key: K, value: V): V { setdefault(key: string, value: V): V {
const mapping = this._items[this._munge(key)]; const mapping = this._items[this._munge(key)];
if (mapping === undefined) { if (mapping === undefined) {
return this.set(key, value); return this.set(key, value);
@ -73,15 +73,15 @@ export class Dict<K, V> {
return mapping.v; return mapping.v;
} }
has(key: K): boolean { has(key: string): boolean {
return _.has(this._items, this._munge(key)); return _.has(this._items, this._munge(key));
} }
del(key: K): void { del(key: string): void {
delete this._items[this._munge(key)]; delete this._items[this._munge(key)];
} }
keys(): K[] { keys(): string[] {
return _.pluck(_.values(this._items), 'k'); return _.pluck(_.values(this._items), 'k');
} }
@ -89,9 +89,9 @@ export class Dict<K, V> {
return _.pluck(_.values(this._items), 'v'); return _.pluck(_.values(this._items), 'v');
} }
items(): [K, V][] { items(): [string, V][] {
return _.map(_.values(this._items), return _.map(_.values(this._items),
(mapping: KeyValue<K, V>): [K, V] => [mapping.k, mapping.v]); (mapping: KeyValue<V>): [string, V] => [mapping.k, mapping.v]);
} }
num_items(): number { num_items(): number {
@ -102,8 +102,8 @@ export class Dict<K, V> {
return _.isEmpty(this._items); return _.isEmpty(this._items);
} }
each(f: (v: V, k?: K) => void): void { each(f: (v: V, k?: string) => void): void {
_.each(this._items, (mapping: KeyValue<K, V>) => f(mapping.v, mapping.k)); _.each(this._items, (mapping: KeyValue<V>) => f(mapping.v, mapping.k));
} }
clear(): void { clear(): void {
@ -111,12 +111,17 @@ export class Dict<K, V> {
} }
// Convert keys to strings and handle undefined. // Convert keys to strings and handle undefined.
private _munge(key: K): string | undefined { private _munge(key: string): string | undefined {
if (key === undefined) { if (key === undefined) {
blueslip.error("Tried to call a Dict method with an undefined key."); blueslip.error("Tried to call a Dict method with an undefined key.");
return undefined; return undefined;
} }
const str_key = ':' + key.toString();
return str_key; if (typeof key !== 'string') {
blueslip.error("Tried to call a Dict method with a non-string.");
key = (key as object).toString();
}
return ':' + key;
} }
} }