Skip to content

Commit 864c811

Browse files
Josh GoldbergJosh Goldbergbradzacher
committed
feat(eslint-plugin): added new rule no-dynamic-delete (typescript-eslint#565)
Co-authored-by: Josh Goldberg <josh@fullscreenmario.com> Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
1 parent 62b5a94 commit 864c811

File tree

7 files changed

+269
-1
lines changed

7 files changed

+269
-1
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
160160
| [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | |
161161
| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | |
162162
| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | |
163+
| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Bans usage of the delete operator with computed key expressions | | :wrench: | |
163164
| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | |
164165
| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | | |
165166
| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | |

packages/eslint-plugin/ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
| [`no-duplicate-super`] | 🌟 | [`constructor-super`][constructor-super] |
6161
| [`no-duplicate-switch-case`] | 🌟 | [`no-duplicate-case`][no-duplicate-case] |
6262
| [`no-duplicate-variable`] | 🌟 | [`no-redeclare`][no-redeclare] |
63-
| [`no-dynamic-delete`] | 🛑 | N/A |
63+
| [`no-dynamic-delete`] | | [`@typescript-eslint/no-dynamic-delete`] |
6464
| [`no-empty`] | 🌟 | [`no-empty`][no-empty] |
6565
| [`no-eval`] | 🌟 | [`no-eval`][no-eval] |
6666
| [`no-floating-promises`] || [`@typescript-eslint/no-floating-promises`] |
@@ -613,6 +613,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
613613
[`@typescript-eslint/member-delimiter-style`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
614614
[`@typescript-eslint/prefer-for-of`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-for-of.md
615615
[`@typescript-eslint/no-array-constructor`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md
616+
[`@typescript-eslint/no-dynamic-delete`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-dynamic-delete.md
616617
[`@typescript-eslint/prefer-function-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-function-type.md
617618
[`@typescript-eslint/prefer-readonly`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly.md
618619
[`@typescript-eslint/require-await`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/require-await.md
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Disallow the delete operator with computed key expressions (no-dynamic-delete)
2+
3+
Deleting dynamically computed keys can be dangerous and in some cases not well optimized.
4+
5+
## Rule Details
6+
7+
Using the `delete` operator on keys that aren't runtime constants could be a sign that you're using the wrong data structures.
8+
Using `Object`s with added and removed keys can cause occasional edge case bugs, such as if a key is named `"hasOwnProperty"`.
9+
Consider using a `Map` or `Set` if you’re storing collections of objects.
10+
11+
Examples of **correct** code wth this rule:
12+
13+
```ts
14+
const container: { [i: string]: number } = {
15+
/* ... */
16+
};
17+
18+
// Constant runtime lookups by string index
19+
delete container.aaa;
20+
21+
// Constants that must be accessed by []
22+
delete container[7];
23+
delete container['-Infinity'];
24+
```
25+
26+
Examples of **incorrect** code with this rule:
27+
28+
```ts
29+
// Can be replaced with the constant equivalents, such as container.aaa
30+
delete container['aaa'];
31+
delete container['Infinity'];
32+
33+
// Dynamic, difficult-to-reason-about lookups
34+
const name = 'name';
35+
delete container[name];
36+
delete container[name.toUpperCase()];
37+
```
38+
39+
## When Not To Use It
40+
41+
When you know your keys are safe to delete, this rule can be unnecessary.
42+
Some environments such as older browsers might not support `Map` and `Set`.
43+
44+
Do not consider this rule as performance advice before profiling your code's bottlenecks.
45+
Even repeated minor performance slowdowns likely do not significantly affect your application's general perceived speed.
46+
47+
## Related to
48+
49+
- TSLint: [no-dynamic-delete](https://palantir.github.io/tslint/rules/no-dynamic-delete)

packages/eslint-plugin/src/configs/all.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@typescript-eslint/member-ordering": "error",
2727
"no-array-constructor": "off",
2828
"@typescript-eslint/no-array-constructor": "error",
29+
"@typescript-eslint/no-dynamic-delete": "error",
2930
"no-empty-function": "off",
3031
"@typescript-eslint/no-empty-function": "error",
3132
"@typescript-eslint/no-empty-interface": "error",

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import memberDelimiterStyle from './member-delimiter-style';
1818
import memberNaming from './member-naming';
1919
import memberOrdering from './member-ordering';
2020
import noArrayConstructor from './no-array-constructor';
21+
import noDynamicDelete from './no-dynamic-delete';
2122
import noEmptyFunction from './no-empty-function';
2223
import noEmptyInterface from './no-empty-interface';
2324
import noExplicitAny from './no-explicit-any';
@@ -85,6 +86,7 @@ export default {
8586
'member-naming': memberNaming,
8687
'member-ordering': memberOrdering,
8788
'no-array-constructor': noArrayConstructor,
89+
'no-dynamic-delete': noDynamicDelete,
8890
'no-empty-function': noEmptyFunction,
8991
'no-empty-interface': noEmptyInterface,
9092
'no-explicit-any': noExplicitAny,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
TSESTree,
3+
AST_NODE_TYPES,
4+
TSESLint,
5+
} from '@typescript-eslint/experimental-utils';
6+
import * as tsutils from 'tsutils';
7+
import * as util from '../util';
8+
9+
export default util.createRule({
10+
name: 'no-dynamic-delete',
11+
meta: {
12+
docs: {
13+
category: 'Best Practices',
14+
description:
15+
'Bans usage of the delete operator with computed key expressions',
16+
recommended: false,
17+
},
18+
fixable: 'code',
19+
messages: {
20+
dynamicDelete: 'Do not delete dynamically computed property keys.',
21+
},
22+
schema: [],
23+
type: 'suggestion',
24+
},
25+
defaultOptions: [],
26+
create(context) {
27+
function createFixer(
28+
member: TSESTree.MemberExpression,
29+
): TSESLint.ReportFixFunction | undefined {
30+
if (
31+
member.property.type === AST_NODE_TYPES.Literal &&
32+
typeof member.property.value === 'string'
33+
) {
34+
return createPropertyReplacement(
35+
member.property,
36+
member.property.value,
37+
);
38+
}
39+
40+
if (member.property.type === AST_NODE_TYPES.Identifier) {
41+
return createPropertyReplacement(member.property, member.property.name);
42+
}
43+
44+
return undefined;
45+
}
46+
47+
return {
48+
'UnaryExpression[operator=delete]'(node: TSESTree.UnaryExpression): void {
49+
if (
50+
node.argument.type !== AST_NODE_TYPES.MemberExpression ||
51+
!node.argument.computed ||
52+
isNecessaryDynamicAccess(
53+
diveIntoWrapperExpressions(node.argument.property),
54+
)
55+
) {
56+
return;
57+
}
58+
59+
context.report({
60+
fix: createFixer(node.argument),
61+
messageId: 'dynamicDelete',
62+
node: node.argument.property,
63+
});
64+
},
65+
};
66+
67+
function createPropertyReplacement(
68+
property: TSESTree.Expression,
69+
replacement: string,
70+
) {
71+
return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix =>
72+
fixer.replaceTextRange(getTokenRange(property), `.${replacement}`);
73+
}
74+
75+
function getTokenRange(property: TSESTree.Expression): [number, number] {
76+
const sourceCode = context.getSourceCode();
77+
78+
return [
79+
sourceCode.getTokenBefore(property)!.range[0],
80+
sourceCode.getTokenAfter(property)!.range[1],
81+
];
82+
}
83+
},
84+
});
85+
86+
function diveIntoWrapperExpressions(
87+
node: TSESTree.Expression,
88+
): TSESTree.Expression {
89+
if (node.type === AST_NODE_TYPES.UnaryExpression) {
90+
return diveIntoWrapperExpressions(node.argument);
91+
}
92+
93+
return node;
94+
}
95+
96+
function isNecessaryDynamicAccess(property: TSESTree.Expression): boolean {
97+
if (property.type !== AST_NODE_TYPES.Literal) {
98+
return false;
99+
}
100+
101+
if (typeof property.value === 'number') {
102+
return true;
103+
}
104+
105+
return (
106+
typeof property.value === 'string' &&
107+
!tsutils.isValidPropertyAccess(property.value)
108+
);
109+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import path from 'path';
2+
import rule from '../../src/rules/no-dynamic-delete';
3+
import { RuleTester } from '../RuleTester';
4+
5+
const rootDir = path.join(process.cwd(), 'tests/fixtures');
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 2015,
9+
tsconfigRootDir: rootDir,
10+
project: './tsconfig.json',
11+
},
12+
parser: '@typescript-eslint/parser',
13+
});
14+
15+
ruleTester.run('no-dynamic-delete', rule, {
16+
valid: [
17+
`const container: { [i: string]: 0 } = {};
18+
delete container.aaa;`,
19+
`const container: { [i: string]: 0 } = {};
20+
delete container.delete;`,
21+
`const container: { [i: string]: 0 } = {};
22+
delete container[7];`,
23+
`const container: { [i: string]: 0 } = {};
24+
delete container[-7];`,
25+
`const container: { [i: string]: 0 } = {};
26+
delete container[+7];`,
27+
`const container: { [i: string]: 0 } = {};
28+
delete container['-Infinity'];`,
29+
`const container: { [i: string]: 0 } = {};
30+
delete container['+Infinity'];`,
31+
`const value = 1;
32+
delete value;`,
33+
`const value = 1;
34+
delete -value;`,
35+
],
36+
invalid: [
37+
{
38+
code: `const container: { [i: string]: 0 } = {};
39+
delete container['aaa'];`,
40+
errors: [{ messageId: 'dynamicDelete' }],
41+
output: `const container: { [i: string]: 0 } = {};
42+
delete container.aaa;`,
43+
},
44+
{
45+
code: `const container: { [i: string]: 0 } = {};
46+
delete container [ 'aaa' ] ;`,
47+
errors: [{ messageId: 'dynamicDelete' }],
48+
output: `const container: { [i: string]: 0 } = {};
49+
delete container .aaa ;`,
50+
},
51+
{
52+
code: `const container: { [i: string]: 0 } = {};
53+
delete container['aa' + 'b'];`,
54+
errors: [{ messageId: 'dynamicDelete' }],
55+
},
56+
{
57+
code: `const container: { [i: string]: 0 } = {};
58+
delete container['delete'];`,
59+
errors: [{ messageId: 'dynamicDelete' }],
60+
output: `const container: { [i: string]: 0 } = {};
61+
delete container.delete;`,
62+
},
63+
{
64+
code: `const container: { [i: string]: 0 } = {};
65+
delete container[-Infinity];`,
66+
errors: [{ messageId: 'dynamicDelete' }],
67+
},
68+
{
69+
code: `const container: { [i: string]: 0 } = {};
70+
delete container[+Infinity];`,
71+
errors: [{ messageId: 'dynamicDelete' }],
72+
},
73+
{
74+
code: `const container: { [i: string]: 0 } = {};
75+
delete container[NaN];`,
76+
errors: [{ messageId: 'dynamicDelete' }],
77+
},
78+
{
79+
code: `const container: { [i: string]: 0 } = {};
80+
delete container['NaN'];`,
81+
errors: [{ messageId: 'dynamicDelete' }],
82+
output: `const container: { [i: string]: 0 } = {};
83+
delete container.NaN;`,
84+
},
85+
{
86+
code: `const container: { [i: string]: 0 } = {};
87+
delete container [ 'NaN' ] ;`,
88+
errors: [{ messageId: 'dynamicDelete' }],
89+
output: `const container: { [i: string]: 0 } = {};
90+
delete container .NaN ;`,
91+
},
92+
{
93+
code: `const container: { [i: string]: 0 } = {};
94+
const name = 'name';
95+
delete container[name];`,
96+
errors: [{ messageId: 'dynamicDelete' }],
97+
},
98+
{
99+
code: `const container: { [i: string]: 0 } = {};
100+
const getName = () => 'aaa';
101+
delete container[getName()];`,
102+
errors: [{ messageId: 'dynamicDelete' }],
103+
},
104+
],
105+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy