diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index baec8f2426e6..be3991f9e7c6 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -145,7 +145,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | +| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | | [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | | | [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions. | :heavy_check_mark: | | | @@ -160,6 +160,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Bans usage of the delete operator with computed key expressions. | | | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/no-dynamic-delete.md b/packages/eslint-plugin/docs/rules/no-dynamic-delete.md new file mode 100644 index 000000000000..4f1bda39616e --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-dynamic-delete.md @@ -0,0 +1,27 @@ +# no-dynamic-delete + +Bans usage of the delete operator with computed key expressions. + +## Rule Details + +Deleting dynamically computed keys is dangerous and not well optimized. + +Also consider using a `Map` or `Set` if you’re storing collections of objects. Using +`Object`s can cause occasional edge case bugs, such as if a key is named “hasOwnProperty”. + +### Options + +Not configurable + +## When Not To Use It + +If you require deleting computed object property keys, consider +setting the property value to `undefined` instead, or creating a new +object without the property you wish to delete. + +If those alternatives do not work for you, you should use `delete` and +disable this rule. + +## Related to + +- TSLint: [`no-dynamic-delete`](https://palantir.github.io/tslint/rules/no-dynamic-delete/) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 377f4b58f0e4..8713d848d39d 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -26,6 +26,7 @@ "@typescript-eslint/member-ordering": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-dynamic-delete": "error", "no-empty-function": "off", "@typescript-eslint/no-empty-function": "error", "@typescript-eslint/no-empty-interface": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5302abd05de0..8b144886b2b7 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -62,6 +62,7 @@ import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; import useDefaultTypeParameter from './no-unnecessary-type-arguments'; +import noDynamicDelete from './no-dynamic-delete'; export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, @@ -84,6 +85,7 @@ export default { 'member-naming': memberNaming, 'member-ordering': memberOrdering, 'no-array-constructor': noArrayConstructor, + 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, 'no-explicit-any': noExplicitAny, diff --git a/packages/eslint-plugin/src/rules/no-dynamic-delete.ts b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts new file mode 100644 index 000000000000..b69dd58aaed3 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts @@ -0,0 +1,37 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +export default util.createRule({ + name: 'no-dynamic-delete', + meta: { + type: 'problem', + docs: { + description: + 'Bans usage of the delete operator with computed key expressions.', + category: 'Possible Errors', + recommended: false, + requiresTypeChecking: false, + }, + messages: { + noDynamicDelete: 'Do not delete dynamically computed property keys.', + }, + schema: [], + }, + defaultOptions: [null], + create(context) { + return { + UnaryExpression(node: TSESTree.UnaryExpression): void { + if (node.operator !== 'delete') { + return; + } + + if ((node.argument as TSESTree.MemberExpression).computed) { + context.report({ + node, + messageId: 'noDynamicDelete', + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts b/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts new file mode 100644 index 000000000000..d9ff2815d156 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-dynamic-delete.test.ts @@ -0,0 +1,77 @@ +import rule from '../../src/rules/no-dynamic-delete'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const messageId = 'noDynamicDelete'; + +ruleTester.run('no-dynamic-delete', rule, { + valid: [ + { + code: 'delete container.a;', + }, + { + code: 'delete container.b;', + }, + ], + invalid: [ + { + code: 'delete container["a" + "b"];', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container["c"]', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container[7]', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container[+7]', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container[-7]', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container[NaN]', + errors: [ + { + messageId, + }, + ], + }, + { + code: 'delete container[getComputedName()]', + errors: [ + { + messageId, + }, + ], + }, + ], +});