From e4259d48a58ef411e32da9b411b962cb71ba7b04 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 31 Jan 2020 20:10:50 -0800 Subject: [PATCH] dict: Assert that Dict is only used with string keys. Signed-off-by: Anders Kaseorg --- .eslintrc.json | 2 ++ frontend_tests/node_tests/dict.js | 12 +++++++ static/js/dict.ts | 53 +++++++++++++++++-------------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index dff3968064..1a494a6060 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -434,6 +434,7 @@ "empty-returns/main": "off", "indent": "off", "func-call-spacing": "off", + "no-extra-parens": "off", "no-magic-numbers": "off", "semi": "off", "no-unused-vars": "off", @@ -459,6 +460,7 @@ "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extra-parens": ["error", "all"], "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-for-in-array": "off", "@typescript-eslint/no-inferrable-types": "error", diff --git a/frontend_tests/node_tests/dict.js b/frontend_tests/node_tests/dict.js index 3cc3f68302..200f904b71 100644 --- a/frontend_tests/node_tests/dict.js +++ b/frontend_tests/node_tests/dict.js @@ -40,6 +40,7 @@ run_test('basic', () => { }); run_test('undefined_keys', () => { + blueslip.clear_test_data(); blueslip.set_test_data('error', 'Tried to call a Dict method with an undefined key.'); const d = new Dict(); @@ -49,6 +50,17 @@ run_test('undefined_keys', () => { 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', () => { const d = new Dict(); diff --git a/static/js/dict.ts b/static/js/dict.ts index 5871cbde24..0a6cd844f0 100644 --- a/static/js/dict.ts +++ b/static/js/dict.ts @@ -1,23 +1,23 @@ import * as _ from 'underscore'; -type KeyValue = { k: K; v: V }; -type Items = { - [key: string]: KeyValue; +type KeyValue = { k: string; v: V }; +type Items = { + [key: string]: KeyValue; }; -export class Dict { - private _items: Items = {}; +export class Dict { + private _items: Items = {}; /** * Constructs a Dict object from an existing object's keys and values. * @param obj - A javascript object */ - static from(obj: { [key: string]: V }): Dict { + static from(obj: { [key: string]: V }): Dict { if (typeof obj !== "object" || obj === null) { throw new TypeError("Cannot convert argument to Dict"); } - const dict = new Dict(); + const dict = new Dict(); _.each(obj, function (val: V, key: string) { dict.set(key, val); }); @@ -30,25 +30,25 @@ export class Dict { * Intended for use as a set data structure. * @param arr - An array of keys */ - static from_array(arr: K[]): Dict { + static from_array(arr: string[]): Dict { if (!(arr instanceof Array)) { throw new TypeError("Argument is not an array"); } - const dict = new Dict(); + const dict = new Dict(); for (const key of arr) { dict.set(key, true); } return dict; } - clone(): Dict { - const dict = new Dict(); + clone(): Dict { + const dict = new Dict(); dict._items = { ...this._items }; return dict; } - get(key: K): V | undefined { + get(key: string): V | undefined { const mapping = this._items[this._munge(key)]; if (mapping === undefined) { return undefined; @@ -56,7 +56,7 @@ export class Dict { return mapping.v; } - set(key: K, value: V): V { + set(key: string, value: V): V { this._items[this._munge(key)] = {k: key, v: value}; return value; } @@ -65,7 +65,7 @@ export class Dict { * If `key` exists in the Dict, return its value. Otherwise insert `key` * 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)]; if (mapping === undefined) { return this.set(key, value); @@ -73,15 +73,15 @@ export class Dict { return mapping.v; } - has(key: K): boolean { + has(key: string): boolean { return _.has(this._items, this._munge(key)); } - del(key: K): void { + del(key: string): void { delete this._items[this._munge(key)]; } - keys(): K[] { + keys(): string[] { return _.pluck(_.values(this._items), 'k'); } @@ -89,9 +89,9 @@ export class Dict { return _.pluck(_.values(this._items), 'v'); } - items(): [K, V][] { + items(): [string, V][] { return _.map(_.values(this._items), - (mapping: KeyValue): [K, V] => [mapping.k, mapping.v]); + (mapping: KeyValue): [string, V] => [mapping.k, mapping.v]); } num_items(): number { @@ -102,8 +102,8 @@ export class Dict { return _.isEmpty(this._items); } - each(f: (v: V, k?: K) => void): void { - _.each(this._items, (mapping: KeyValue) => f(mapping.v, mapping.k)); + each(f: (v: V, k?: string) => void): void { + _.each(this._items, (mapping: KeyValue) => f(mapping.v, mapping.k)); } clear(): void { @@ -111,12 +111,17 @@ export class Dict { } // Convert keys to strings and handle undefined. - private _munge(key: K): string | undefined { + private _munge(key: string): string | undefined { if (key === undefined) { blueslip.error("Tried to call a Dict method with an undefined key."); 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; } }