From 6ec086df95e791e9866776f4b4c35f60179dac5f Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 00:00:14 +0330 Subject: [PATCH 01/22] Complete Super Method Checking I rewrite it again after one month, and i figured out that the last clone is outdated and i cloned the latest repo and just copied & pasted the file. Because of that there is no history available for this project. --- .../docs/rules/call-super-on-override.md | 65 +++++++++ packages/eslint-plugin/src/configs/all.ts | 1 + .../eslint-plugin/src/configs/recommended.ts | 1 + .../src/rules/call-super-on-override.ts | 129 +++++++++++++++++ packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/call-super-on-override.test.ts | 136 ++++++++++++++++++ 6 files changed, 334 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/call-super-on-override.md create mode 100644 packages/eslint-plugin/src/rules/call-super-on-override.ts create mode 100644 packages/eslint-plugin/tests/rules/call-super-on-override.test.ts diff --git a/packages/eslint-plugin/docs/rules/call-super-on-override.md b/packages/eslint-plugin/docs/rules/call-super-on-override.md new file mode 100644 index 000000000000..001ba60de0f9 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/call-super-on-override.md @@ -0,0 +1,65 @@ +--- +description: 'Require overridden methods to call super.method in their body.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/call-super-on-override** for documentation. + +This rule enforces that overridden methods are calling exact super method to avoid missing super class method implementations. + +## Rule Details + +Examples of code for this rule: + +### ❌ Incorrect + +```ts +class Foo1 { + bar(param: any): void {} +} + +class Foo2 extends Foo1 { + override bar(param: any): void {} +} +``` + +### ✅ Correct + +```ts +class Foo1 { + bar(param: any): void {} +} + +class Foo2 extends Foo1 { + override bar(param: any): void { + super.bar(param); + } +} +``` + +### Note + +Class properties that have `function` value are not considered as real method override and cause `SyntaxError` on super calls: + +```ts +class Foo1 { + bar1 = function () {}; + + bar2 = () => {}; +} + +class Foo2 extends Foo1 { + override bar1 = function () { + super.bar1(); // SyntaxError: 'super' keyword unexpected here + }; + + override bar2 = () => { + super.bar2(); // SyntaxError: 'super' keyword unexpected here + }; +} +``` + +## When Not To Use It + +When you are using TypeScript < 4.3 or you did not set `noImplicitOverride: true` in `CompilerOptions` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 8742d36b2089..dc5a8508d6ea 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -13,6 +13,7 @@ export = { '@typescript-eslint/ban-types': 'error', 'brace-style': 'off', '@typescript-eslint/brace-style': 'error', + '@typescript-eslint/call-super-on-override': 'error', '@typescript-eslint/class-literal-property-style': 'error', 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 10b1d04581fb..6a78e60faca7 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -8,6 +8,7 @@ export = { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/ban-types': 'error', + '@typescript-eslint/call-super-on-override': 'warn', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', 'no-empty-function': 'off', diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts new file mode 100644 index 000000000000..ec6a4cf42cd0 --- /dev/null +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -0,0 +1,129 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import * as utils from '../util'; + +type Options = [ + { + ignoreMethods: boolean; + topLevel: boolean; + }, +]; + +type MessageIds = 'missingSuperMethodCall' | 'topLevelSuperMethodCall'; + +export default utils.createRule({ + name: 'call-super-on-override', + meta: { + type: 'suggestion', + docs: { + description: + 'Require overridden methods to call super.method in their body', + recommended: 'warn', + requiresTypeChecking: false, + }, + messages: { + missingSuperMethodCall: + "Use 'super.{{property}}{{parameterTuple}}' to avoid missing super class method implementations", + topLevelSuperMethodCall: + "super method must be called before accessing 'this' in the overridden method of a derived class.", + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + ignoreMethods: { + type: 'boolean', + }, + topLevel: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + ignoreMethods: false, + topLevel: false, + }, + ], + create(context, [{ ignoreMethods, topLevel }]) { + return { + 'MethodDefinition[override=true][kind="method"]'( + node: TSESTree.MethodDefinition, + ): void { + if (ignoreMethods) { + return; + } + + const { name: methodName } = node.key as TSESTree.Identifier, + bodyStatements = node.value.body!.body; + + // Search for super method call + let thisWasAccessed = false; + for (const statement of bodyStatements) { + if (!thisWasAccessed && isThisAccess(statement)) { + thisWasAccessed = true; + } + + if (isSuperMethodCall(statement, methodName)) { + if (topLevel && thisWasAccessed) { + context.report({ + messageId: 'topLevelSuperMethodCall', + node: bodyStatements[0], + }); + } + + return; // We are done here, missingSuperMethodCall error + } + } + + // Raise if not found + context.report({ + messageId: 'missingSuperMethodCall', + node: node, + data: { + property: methodName, + parameterTuple: `(${node.value.params + .map(p => (p as TSESTree.Identifier).name) + .join(', ')})`, + }, + }); + }, + }; + }, +}); + +const isSuperMethodCall = ( + statement: TSESTree.Statement | undefined, + methodName: string, +): boolean => + statement != null && + statement.type === AST_NODE_TYPES.ExpressionStatement && + statement.expression.type === AST_NODE_TYPES.CallExpression && + statement.expression.callee.type === AST_NODE_TYPES.MemberExpression && + statement.expression.callee.object.type === AST_NODE_TYPES.Super && + statement.expression.callee.property.type === AST_NODE_TYPES.Identifier && + statement.expression.callee.property.name === methodName; + +const isThisAccess = (statement: TSESTree.Statement): boolean => { + if (!isCallExpression(statement)) { + return false; + } + + let obj = (statement.expression as TSESTree.CallExpression).callee; + + // Going deeper and deeper + while (obj.type === AST_NODE_TYPES.MemberExpression) { + obj = (obj as TSESTree.MemberExpression).object; + } + + return obj.type === AST_NODE_TYPES.ThisExpression; +}; + +const isCallExpression = ( + statement: TSESTree.Statement, +): statement is TSESTree.ExpressionStatement => + statement.type === AST_NODE_TYPES.ExpressionStatement && + statement.expression.type === AST_NODE_TYPES.CallExpression; diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 29a47ec2384a..47667c940987 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -5,6 +5,7 @@ import banTsComment from './ban-ts-comment'; import banTslintComment from './ban-tslint-comment'; import banTypes from './ban-types'; import braceStyle from './brace-style'; +import callSuperOnOverride from './call-super-on-override'; import classLiteralPropertyStyle from './class-literal-property-style'; import commaDangle from './comma-dangle'; import commaSpacing from './comma-spacing'; @@ -134,6 +135,7 @@ export default { 'ban-tslint-comment': banTslintComment, 'ban-types': banTypes, 'brace-style': braceStyle, + 'call-super-on-override': callSuperOnOverride, 'class-literal-property-style': classLiteralPropertyStyle, 'comma-dangle': commaDangle, 'comma-spacing': commaSpacing, diff --git a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts new file mode 100644 index 000000000000..94aaac476a5f --- /dev/null +++ b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts @@ -0,0 +1,136 @@ +import { RuleTester } from '../RuleTester'; +import rule from '../../src/rules/call-super-on-override'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('call-super-on-override', rule, { + valid: [ + { + code: ` +class ValidSample { + override x() {} +} + `, + options: [ + // off + { + ignoreMethods: true, + topLevel: false, + }, + ], + }, + { + code: ` +class ValidSample { + override x() { + this.y(); + super.x(); + } +} + `, + options: [ + // raise only for super absence + { + ignoreMethods: false, + topLevel: false, + }, + ], + }, + { + code: ` +class ValidSample { + override x() { + super.x(); + this.y(); + } +} + `, + options: [ + // raise for ordering + { + ignoreMethods: false, + topLevel: true, + }, + ], + }, + { + code: ` +class ValidSample { + override x() { + p(); + l(); + super.x(); + this.y(); + } +} + `, + options: [ + // not raise for ordering + { + ignoreMethods: false, + topLevel: true, + }, + ], + }, + ], + invalid: [ + { + code: ` +class InvalidSample { + override x() { + this.x(); + super.x = () => void 0; + super.x; + } +} + `, + options: [ + // raise only for super call absence + { + ignoreMethods: false, + topLevel: false, + }, + ], + errors: [{ messageId: 'missingSuperMethodCall' }], + }, + { + code: ` +class InvalidSample { + override x() { + super.x = () => void 0; + this.x(); + super.x(); + } +} + `, + options: [ + // raise only for top level super call absence + { + ignoreMethods: false, + topLevel: true, + }, + ], + errors: [{ messageId: 'topLevelSuperMethodCall' }], + }, + { + code: ` +class ValidSample { + override x() { + this.x.y.z.c.v.b.n.l(); + super.x(); + } +} + `, + options: [ + // not raise for ordering + { + ignoreMethods: false, + topLevel: true, + }, + ], + errors: [{ messageId: 'topLevelSuperMethodCall' }], + }, + ], +}); From 31be88ce2727510fbfa854ae866407c8b3444f3a Mon Sep 17 00:00:00 2001 From: SoR <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:57:32 +0330 Subject: [PATCH 02/22] Update packages/eslint-plugin/docs/rules/call-super-on-override.md Unnecessary Information Removed Co-authored-by: Josh Goldberg --- .../docs/rules/call-super-on-override.md | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/call-super-on-override.md b/packages/eslint-plugin/docs/rules/call-super-on-override.md index 001ba60de0f9..6c6557494bf9 100644 --- a/packages/eslint-plugin/docs/rules/call-super-on-override.md +++ b/packages/eslint-plugin/docs/rules/call-super-on-override.md @@ -38,27 +38,6 @@ class Foo2 extends Foo1 { } ``` -### Note - -Class properties that have `function` value are not considered as real method override and cause `SyntaxError` on super calls: - -```ts -class Foo1 { - bar1 = function () {}; - - bar2 = () => {}; -} - -class Foo2 extends Foo1 { - override bar1 = function () { - super.bar1(); // SyntaxError: 'super' keyword unexpected here - }; - - override bar2 = () => { - super.bar2(); // SyntaxError: 'super' keyword unexpected here - }; -} -``` ## When Not To Use It From 5138d1d45c624faf09e5a263e713505906dfa582 Mon Sep 17 00:00:00 2001 From: SoR <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:58:11 +0330 Subject: [PATCH 03/22] Update packages/eslint-plugin/src/rules/call-super-on-override.ts Rule Is Now Off By Default Co-authored-by: Josh Goldberg --- packages/eslint-plugin/src/rules/call-super-on-override.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts index ec6a4cf42cd0..e610530d227a 100644 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -17,7 +17,7 @@ export default utils.createRule({ docs: { description: 'Require overridden methods to call super.method in their body', - recommended: 'warn', + recommended: false, requiresTypeChecking: false, }, messages: { From b8444c3f451c018afc046ca821b126a363234ba7 Mon Sep 17 00:00:00 2001 From: SoR <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 10:02:47 +0330 Subject: [PATCH 04/22] Update packages/eslint-plugin/src/rules/call-super-on-override.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My bad!😱 Co-authored-by: Josh Goldberg --- packages/eslint-plugin/src/rules/call-super-on-override.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts index e610530d227a..75bde52c8e5b 100644 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -99,8 +99,7 @@ const isSuperMethodCall = ( statement: TSESTree.Statement | undefined, methodName: string, ): boolean => - statement != null && - statement.type === AST_NODE_TYPES.ExpressionStatement && + statement?.type === AST_NODE_TYPES.ExpressionStatement && statement.expression.type === AST_NODE_TYPES.CallExpression && statement.expression.callee.type === AST_NODE_TYPES.MemberExpression && statement.expression.callee.object.type === AST_NODE_TYPES.Super && From a31ade2adf179225104b44e8352192a3e582e791 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 10:13:49 +0330 Subject: [PATCH 05/22] Unnecessary ignoreMethods Option Removed This option is unneeded because users can simply turn off the rule --- .../src/rules/call-super-on-override.ts | 11 +----- .../rules/call-super-on-override.test.ts | 36 ++----------------- 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts index 75bde52c8e5b..e26157cd5575 100644 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -3,7 +3,6 @@ import * as utils from '../util'; type Options = [ { - ignoreMethods: boolean; topLevel: boolean; }, ]; @@ -31,9 +30,6 @@ export default utils.createRule({ { type: 'object', properties: { - ignoreMethods: { - type: 'boolean', - }, topLevel: { type: 'boolean', }, @@ -44,19 +40,14 @@ export default utils.createRule({ }, defaultOptions: [ { - ignoreMethods: false, topLevel: false, }, ], - create(context, [{ ignoreMethods, topLevel }]) { + create(context, [{ topLevel }]) { return { 'MethodDefinition[override=true][kind="method"]'( node: TSESTree.MethodDefinition, ): void { - if (ignoreMethods) { - return; - } - const { name: methodName } = node.key as TSESTree.Identifier, bodyStatements = node.value.body!.body; diff --git a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts index 94aaac476a5f..298406eda4c7 100644 --- a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts +++ b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts @@ -9,20 +9,6 @@ ruleTester.run('call-super-on-override', rule, { valid: [ { code: ` -class ValidSample { - override x() {} -} - `, - options: [ - // off - { - ignoreMethods: true, - topLevel: false, - }, - ], - }, - { - code: ` class ValidSample { override x() { this.y(); @@ -30,13 +16,6 @@ class ValidSample { } } `, - options: [ - // raise only for super absence - { - ignoreMethods: false, - topLevel: false, - }, - ], }, { code: ` @@ -50,7 +29,6 @@ class ValidSample { options: [ // raise for ordering { - ignoreMethods: false, topLevel: true, }, ], @@ -69,7 +47,6 @@ class ValidSample { options: [ // not raise for ordering { - ignoreMethods: false, topLevel: true, }, ], @@ -86,13 +63,6 @@ class InvalidSample { } } `, - options: [ - // raise only for super call absence - { - ignoreMethods: false, - topLevel: false, - }, - ], errors: [{ messageId: 'missingSuperMethodCall' }], }, { @@ -108,7 +78,6 @@ class InvalidSample { options: [ // raise only for top level super call absence { - ignoreMethods: false, topLevel: true, }, ], @@ -124,9 +93,8 @@ class ValidSample { } `, options: [ - // not raise for ordering + // raise only for top level super call absence (deep case) { - ignoreMethods: false, topLevel: true, }, ], @@ -134,3 +102,5 @@ class ValidSample { }, ], }); + +// TODO needs test cases for Literal method name instead of Just Identifier From 2b65003c10167c988b5de88eaf9281a2aff379b0 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 11:44:56 +0330 Subject: [PATCH 06/22] Support For Literal Method Names Support for literal method names & better error message for computed and non-computed method names. --- .../src/rules/call-super-on-override.ts | 68 +++++++++++--- .../rules/call-super-on-override.test.ts | 92 ++++++++++++++++++- 2 files changed, 145 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts index e26157cd5575..f07e25157a2d 100644 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -21,7 +21,7 @@ export default utils.createRule({ }, messages: { missingSuperMethodCall: - "Use 'super.{{property}}{{parameterTuple}}' to avoid missing super class method implementations", + "Use 'super{{property}}{{parameterTuple}}' to avoid missing super class method implementations", topLevelSuperMethodCall: "super method must be called before accessing 'this' in the overridden method of a derived class.", }, @@ -48,7 +48,21 @@ export default utils.createRule({ 'MethodDefinition[override=true][kind="method"]'( node: TSESTree.MethodDefinition, ): void { - const { name: methodName } = node.key as TSESTree.Identifier, + let methodName = '', + methodNameIsLiteral = false, + methodNameIsNull = false; // don't add quotes for error message on [null] case + + if (node.key.type === AST_NODE_TYPES.Identifier) { + methodName = node.key.name; + } else { + methodNameIsLiteral = true; + // null & undefined can be used as property names, undefined counted as Identifier & null as Literal + methodName = + (node.key as TSESTree.Literal).value?.toString() || 'null'; + methodNameIsNull = (node.key as TSESTree.Literal).value == null; + } + + const { computed: isComputed } = node, bodyStatements = node.value.body!.body; // Search for super method call @@ -58,7 +72,13 @@ export default utils.createRule({ thisWasAccessed = true; } - if (isSuperMethodCall(statement, methodName)) { + if ( + isSuperMethodCall( + statement, + methodName, + !methodNameIsLiteral && isComputed, + ) + ) { if (topLevel && thisWasAccessed) { context.report({ messageId: 'topLevelSuperMethodCall', @@ -66,7 +86,7 @@ export default utils.createRule({ }); } - return; // We are done here, missingSuperMethodCall error + return; // We are done here, no missingSuperMethodCall error } } @@ -75,7 +95,13 @@ export default utils.createRule({ messageId: 'missingSuperMethodCall', node: node, data: { - property: methodName, + property: isComputed + ? `[${ + methodNameIsLiteral && !methodNameIsNull + ? `'${methodName}'` + : methodName + }]` + : `.${methodName}`, parameterTuple: `(${node.value.params .map(p => (p as TSESTree.Identifier).name) .join(', ')})`, @@ -89,13 +115,31 @@ export default utils.createRule({ const isSuperMethodCall = ( statement: TSESTree.Statement | undefined, methodName: string, -): boolean => - statement?.type === AST_NODE_TYPES.ExpressionStatement && - statement.expression.type === AST_NODE_TYPES.CallExpression && - statement.expression.callee.type === AST_NODE_TYPES.MemberExpression && - statement.expression.callee.object.type === AST_NODE_TYPES.Super && - statement.expression.callee.property.type === AST_NODE_TYPES.Identifier && - statement.expression.callee.property.name === methodName; + methodIsComputedIdentifier: boolean, +): boolean => { + // for edge cases like this -> override [X]() { super.X() } + // we make sure that computed identifier should have computed callback + let calleeIsComputedIdentifier = false, + calleeNameIsComputed = false; + + const calleeName = + statement?.type === AST_NODE_TYPES.ExpressionStatement && + statement.expression.type === AST_NODE_TYPES.CallExpression && + statement.expression.callee.type === AST_NODE_TYPES.MemberExpression && + statement.expression.callee.object.type === AST_NODE_TYPES.Super && + (statement.expression.callee.property.type === AST_NODE_TYPES.Identifier + ? ((calleeIsComputedIdentifier = statement.expression.callee.computed), + statement.expression.callee.property.name) + : statement.expression.callee.property.type === AST_NODE_TYPES.Literal + ? statement.expression.callee.property.value?.toString() || 'null' + : undefined); + + return methodIsComputedIdentifier + ? calleeIsComputedIdentifier + ? methodName === calleeName + : false + : methodName === calleeName; +}; const isThisAccess = (statement: TSESTree.Statement): boolean => { if (!isCallExpression(statement)) { diff --git a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts index 298406eda4c7..7819dcb5e3b5 100644 --- a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts +++ b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts @@ -19,6 +19,24 @@ class ValidSample { }, { code: ` +class ValidSample { + override ['x-y']() { + super['x-y'](); + } + override ['z']() { + super.z(); + } + override h() { + super['h']() + } + override [M]() { + super[M](); + } +} + `, + }, + { + code: ` class ValidSample { override x() { super.x(); @@ -27,7 +45,7 @@ class ValidSample { } `, options: [ - // raise for ordering + // ordering { topLevel: true, }, @@ -39,7 +57,7 @@ class ValidSample { override x() { p(); l(); - super.x(); + super['x'](); this.y(); } } @@ -63,7 +81,29 @@ class InvalidSample { } } `, - errors: [{ messageId: 'missingSuperMethodCall' }], + errors: [ + { + messageId: 'missingSuperMethodCall', + data: { property: '.x', parameterTuple: '()' }, + }, + ], + }, + { + code: ` +class InvalidSample { + override ['x-y-z']() { + this['x-y-z'](); + super['x-y-z'] = () => void 0; + super['x-y-z']; + } +} + `, + errors: [ + { + messageId: 'missingSuperMethodCall', + data: { property: "['x-y-z']", parameterTuple: '()' }, + }, + ], }, { code: ` @@ -85,6 +125,19 @@ class InvalidSample { }, { code: ` +class InvalidSample { + override x(y: number, z: string) {} +} + `, + errors: [ + { + messageId: 'missingSuperMethodCall', + data: { property: '.x', parameterTuple: '(y, z)' }, + }, + ], + }, + { + code: ` class ValidSample { override x() { this.x.y.z.c.v.b.n.l(); @@ -100,6 +153,39 @@ class ValidSample { ], errors: [{ messageId: 'topLevelSuperMethodCall' }], }, + { + code: ` +class InvalidSample { + override [M]() { + super.M() + } +} + `, + errors: [ + { + messageId: 'missingSuperMethodCall', + data: { property: '[M]', parameterTuple: '()' }, + }, + ], + }, + { + code: ` +class InvalidSample { + override [null]() {} + override ['null']() {} +} + `, + errors: [ + { + messageId: 'missingSuperMethodCall', + data: { property: '[null]', parameterTuple: '()' }, + }, + { + messageId: 'missingSuperMethodCall', + data: { property: "['null']", parameterTuple: '()' }, + }, + ], + }, ], }); From e95c87f754a5e1db606b54cfd6f446d2eef4fa6a Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:05:54 +0330 Subject: [PATCH 07/22] Resolve Failed CI Tests & TopLevel Option Removed --- .../eslint-plugin/src/configs/recommended.ts | 1 - .../src/rules/call-super-on-override.ts | 64 +++------------ .../rules/call-super-on-override.test.ts | 77 +------------------ 3 files changed, 13 insertions(+), 129 deletions(-) diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 6a78e60faca7..10b1d04581fb 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -8,7 +8,6 @@ export = { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/ban-types': 'error', - '@typescript-eslint/call-super-on-override': 'warn', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', 'no-empty-function': 'off', diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts index f07e25157a2d..29100a3f967a 100644 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -1,15 +1,11 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; -import * as utils from '../util'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -type Options = [ - { - topLevel: boolean; - }, -]; +import * as utils from '../util'; -type MessageIds = 'missingSuperMethodCall' | 'topLevelSuperMethodCall'; +type MessageIds = 'missingSuperMethodCall'; -export default utils.createRule({ +export default utils.createRule<[], MessageIds>({ name: 'call-super-on-override', meta: { type: 'suggestion', @@ -22,8 +18,6 @@ export default utils.createRule({ messages: { missingSuperMethodCall: "Use 'super{{property}}{{parameterTuple}}' to avoid missing super class method implementations", - topLevelSuperMethodCall: - "super method must be called before accessing 'this' in the overridden method of a derived class.", }, fixable: 'code', schema: [ @@ -38,12 +32,8 @@ export default utils.createRule({ }, ], }, - defaultOptions: [ - { - topLevel: false, - }, - ], - create(context, [{ topLevel }]) { + defaultOptions: [], + create(context) { return { 'MethodDefinition[override=true][kind="method"]'( node: TSESTree.MethodDefinition, @@ -58,7 +48,7 @@ export default utils.createRule({ methodNameIsLiteral = true; // null & undefined can be used as property names, undefined counted as Identifier & null as Literal methodName = - (node.key as TSESTree.Literal).value?.toString() || 'null'; + (node.key as TSESTree.Literal).value?.toString() ?? 'null'; methodNameIsNull = (node.key as TSESTree.Literal).value == null; } @@ -66,12 +56,7 @@ export default utils.createRule({ bodyStatements = node.value.body!.body; // Search for super method call - let thisWasAccessed = false; for (const statement of bodyStatements) { - if (!thisWasAccessed && isThisAccess(statement)) { - thisWasAccessed = true; - } - if ( isSuperMethodCall( statement, @@ -79,13 +64,6 @@ export default utils.createRule({ !methodNameIsLiteral && isComputed, ) ) { - if (topLevel && thisWasAccessed) { - context.report({ - messageId: 'topLevelSuperMethodCall', - node: bodyStatements[0], - }); - } - return; // We are done here, no missingSuperMethodCall error } } @@ -119,8 +97,7 @@ const isSuperMethodCall = ( ): boolean => { // for edge cases like this -> override [X]() { super.X() } // we make sure that computed identifier should have computed callback - let calleeIsComputedIdentifier = false, - calleeNameIsComputed = false; + let calleeIsComputedIdentifier = false; const calleeName = statement?.type === AST_NODE_TYPES.ExpressionStatement && @@ -131,7 +108,7 @@ const isSuperMethodCall = ( ? ((calleeIsComputedIdentifier = statement.expression.callee.computed), statement.expression.callee.property.name) : statement.expression.callee.property.type === AST_NODE_TYPES.Literal - ? statement.expression.callee.property.value?.toString() || 'null' + ? statement.expression.callee.property.value?.toString() ?? 'null' : undefined); return methodIsComputedIdentifier @@ -140,24 +117,3 @@ const isSuperMethodCall = ( : false : methodName === calleeName; }; - -const isThisAccess = (statement: TSESTree.Statement): boolean => { - if (!isCallExpression(statement)) { - return false; - } - - let obj = (statement.expression as TSESTree.CallExpression).callee; - - // Going deeper and deeper - while (obj.type === AST_NODE_TYPES.MemberExpression) { - obj = (obj as TSESTree.MemberExpression).object; - } - - return obj.type === AST_NODE_TYPES.ThisExpression; -}; - -const isCallExpression = ( - statement: TSESTree.Statement, -): statement is TSESTree.ExpressionStatement => - statement.type === AST_NODE_TYPES.ExpressionStatement && - statement.expression.type === AST_NODE_TYPES.CallExpression; diff --git a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts index 7819dcb5e3b5..03ab3ee02ece 100644 --- a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts +++ b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts @@ -1,5 +1,5 @@ -import { RuleTester } from '../RuleTester'; import rule from '../../src/rules/call-super-on-override'; +import { RuleTester } from '../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -27,7 +27,7 @@ class ValidSample { super.z(); } override h() { - super['h']() + super['h'](); } override [M]() { super[M](); @@ -35,40 +35,6 @@ class ValidSample { } `, }, - { - code: ` -class ValidSample { - override x() { - super.x(); - this.y(); - } -} - `, - options: [ - // ordering - { - topLevel: true, - }, - ], - }, - { - code: ` -class ValidSample { - override x() { - p(); - l(); - super['x'](); - this.y(); - } -} - `, - options: [ - // not raise for ordering - { - topLevel: true, - }, - ], - }, ], invalid: [ { @@ -107,24 +73,6 @@ class InvalidSample { }, { code: ` -class InvalidSample { - override x() { - super.x = () => void 0; - this.x(); - super.x(); - } -} - `, - options: [ - // raise only for top level super call absence - { - topLevel: true, - }, - ], - errors: [{ messageId: 'topLevelSuperMethodCall' }], - }, - { - code: ` class InvalidSample { override x(y: number, z: string) {} } @@ -138,26 +86,9 @@ class InvalidSample { }, { code: ` -class ValidSample { - override x() { - this.x.y.z.c.v.b.n.l(); - super.x(); - } -} - `, - options: [ - // raise only for top level super call absence (deep case) - { - topLevel: true, - }, - ], - errors: [{ messageId: 'topLevelSuperMethodCall' }], - }, - { - code: ` class InvalidSample { override [M]() { - super.M() + super.M(); } } `, @@ -188,5 +119,3 @@ class InvalidSample { }, ], }); - -// TODO needs test cases for Literal method name instead of Just Identifier From 4e9da6857d1f337a2b01c91b22aa9ce6282be082 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Thu, 20 Oct 2022 11:20:57 +0330 Subject: [PATCH 08/22] Rule no-in-array --- .../eslint-plugin/docs/rules/no-in-array.md | 37 +++++++++++++++ packages/eslint-plugin/src/configs/all.ts | 1 + .../recommended-requiring-type-checking.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/no-in-array.ts | 46 +++++++++++++++++++ .../tests/rules/no-in-array.test.ts | 40 ++++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-in-array.md create mode 100644 packages/eslint-plugin/src/rules/no-in-array.ts create mode 100644 packages/eslint-plugin/tests/rules/no-in-array.test.ts diff --git a/packages/eslint-plugin/docs/rules/no-in-array.md b/packages/eslint-plugin/docs/rules/no-in-array.md new file mode 100644 index 000000000000..c8f866647d69 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-in-array.md @@ -0,0 +1,37 @@ +--- +description: 'Disallow using in operator for arrays.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-in-array** for documentation. + +This rule bans using `in` operator for checking array members existence. + +## Rule Details + +Examples of code for this rule: + +### ❌ Incorrect + +```ts +const arr = ['a', 'b', 'c']; + +if ('c' in arr) { + // ... +} +``` + +### ✅ Correct + +```ts +const arr = ['a', 'b', 'c']; + +if (arr.includes('a')) { + // ... +} +``` + +## When Not To Use It + +When you want exactly iterate over array indexes. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 4de48dee1e60..ec184f00355b 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -70,6 +70,7 @@ export = { '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', + '@typescript-eslint/no-in-array': 'error', '@typescript-eslint/no-inferrable-types': 'error', 'no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts index 369d33d6687e..f80e3d3abdaa 100644 --- a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts +++ b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts @@ -10,6 +10,7 @@ export = { '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', + '@typescript-eslint/no-in-array': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unsafe-argument': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index e05d002574c3..8daf8fd54744 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -48,6 +48,7 @@ import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; import noImplicitAnyCatch from './no-implicit-any-catch'; import noImpliedEval from './no-implied-eval'; +import noInArray from './no-in-array'; import noInferrableTypes from './no-inferrable-types'; import noInvalidThis from './no-invalid-this'; import noInvalidVoidType from './no-invalid-void-type'; @@ -179,6 +180,7 @@ export default { 'no-for-in-array': noForInArray, 'no-implicit-any-catch': noImplicitAnyCatch, 'no-implied-eval': noImpliedEval, + 'no-in-array': noInArray, 'no-inferrable-types': noInferrableTypes, 'no-invalid-this': noInvalidThis, 'no-invalid-void-type': noInvalidVoidType, diff --git a/packages/eslint-plugin/src/rules/no-in-array.ts b/packages/eslint-plugin/src/rules/no-in-array.ts new file mode 100644 index 000000000000..7f269c0377b6 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-in-array.ts @@ -0,0 +1,46 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +export default util.createRule<[], 'inArrayViolation'>({ + name: 'no-in-array', + meta: { + docs: { + description: 'Disallow using in operator for arrays', + recommended: 'error', + requiresTypeChecking: true, + }, + messages: { + inArrayViolation: + "'in' operator for arrays is forbidden. Use array.indexOf or array.includes instead.", + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create(context) { + return { + "BinaryExpression[operator='in']"(node: TSESTree.BinaryExpression): void { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); + + const type = util.getConstrainedTypeAtLocation( + checker, + originalNode.right, + ); + + if ( + util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker) || + (type.flags & ts.TypeFlags.StringLike) !== 0 + ) { + context.report({ + node, + messageId: 'inArrayViolation', + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-in-array.test.ts b/packages/eslint-plugin/tests/rules/no-in-array.test.ts new file mode 100644 index 000000000000..984bc9bd7755 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-in-array.test.ts @@ -0,0 +1,40 @@ +import rule from '../../src/rules/no-in-array'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-in-array', rule, { + valid: [ + { + code: ` +if (x in {}) { +} + `, + }, + ], + invalid: [ + { + code: ` +if (x in ['z', 'y']) { +} + `, + errors: [{ messageId: 'inArrayViolation' }], + }, + { + code: ` +const arr = [5, 6, 7, 8]; + +const has_6 = 6 in arr; + `, + errors: [{ messageId: 'inArrayViolation' }], + }, + ], +}); From 75c1979e02b065c1c22b7ae2497a74aee6d10fb5 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:27:16 +0330 Subject: [PATCH 09/22] Rule no-array-delete Completed First commit before PR --- .../docs/rules/no-array-delete.md | 32 +++++++++++ packages/eslint-plugin/src/configs/all.ts | 1 + .../recommended-requiring-type-checking.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-array-delete.ts | 57 +++++++++++++++++++ .../tests/rules/no-array-delete.test.ts | 51 +++++++++++++++++ 6 files changed, 144 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-array-delete.md create mode 100644 packages/eslint-plugin/src/rules/no-array-delete.ts create mode 100644 packages/eslint-plugin/tests/rules/no-array-delete.test.ts diff --git a/packages/eslint-plugin/docs/rules/no-array-delete.md b/packages/eslint-plugin/docs/rules/no-array-delete.md new file mode 100644 index 000000000000..aa13918af9ab --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-array-delete.md @@ -0,0 +1,32 @@ +--- +description: 'Disallow delete operator for arrays.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-array-delete** for documentation. + +In JavaScript `delete` operator in arrays makes the given index empty, and leaves the array length as is. this may +cause a problem for optimizing and undefined behaviors like getting `undefined` when accessing that index. + +## Examples + + + +### ❌ Incorrect + +```ts +declare const array: any[]; +declare const index: number; + +delete array[index]; +``` + +### ✅ Correct + +```js +declare const array: any[]; +declare const index: number; + +array.splice(index, 1); +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index ec184f00355b..d00659dbe177 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -48,6 +48,7 @@ export = { '@typescript-eslint/naming-convention': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', + '@typescript-eslint/no-array-delete': 'error', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-non-null-assertion': 'error', '@typescript-eslint/no-confusing-void-expression': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts index f80e3d3abdaa..d8e260a1800b 100644 --- a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts +++ b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts @@ -6,6 +6,7 @@ export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-array-delete': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 8daf8fd54744..f82333813a14 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -30,6 +30,7 @@ import memberOrdering from './member-ordering'; import methodSignatureStyle from './method-signature-style'; import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; +import noArrayDelete from './no-array-delete'; import noBaseToString from './no-base-to-string'; import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion'; import noConfusingVoidExpression from './no-confusing-void-expression'; @@ -162,6 +163,7 @@ export default { 'method-signature-style': methodSignatureStyle, 'naming-convention': namingConvention, 'no-array-constructor': noArrayConstructor, + 'no-array-delete': noArrayDelete, 'no-base-to-string': noBaseToString, 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, 'no-confusing-void-expression': noConfusingVoidExpression, diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts new file mode 100644 index 000000000000..4a6b6d7eb6b7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -0,0 +1,57 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +export default util.createRule<[], 'arrayDeleteViolation'>({ + name: 'no-array-delete', + meta: { + docs: { + description: 'Disallow delete operator for arrays', + recommended: 'error', + requiresTypeChecking: true, + }, + messages: { + arrayDeleteViolation: + 'Using delete operator for arrays are forbidden. Use array.splice instead.', + }, + schema: [], + type: 'problem', + fixable: 'code', + }, + defaultOptions: [], + create(context) { + return { + "UnaryExpression[operator='delete']"( + node: TSESTree.UnaryExpression, + ): void { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const expression = originalNode.getChildAt(1); + + const target = expression.getChildAt(0), + key = expression.getChildAt(2); + + const targetType = util.getConstrainedTypeAtLocation(checker, target), + keyType = util.getConstrainedTypeAtLocation(checker, key); + + if ( + util.isTypeAnyArrayType(targetType, checker) && + (keyType.flags & ts.TypeFlags.Number) !== 0 + ) { + context.report({ + node, + messageId: 'arrayDeleteViolation', + fix(fixer) { + return fixer.replaceText( + node, + `${target.getText()}.splice(${key.getText()}, 1)`, + ); + }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts new file mode 100644 index 000000000000..6d39e56b0a1c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -0,0 +1,51 @@ +import rule from '../../src/rules/no-array-delete'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-array-delete', rule, { + valid: [ + ` +declare const obj: Record; +declare const key: PropertyKey; + +delete obj[key]; + `, + ` +declare const arr: any[]; + +delete arr.myprop; + `, + ` +declare const arr: any[]; +declare const i: string; + +delete arr[i]; + `, + ], + invalid: [ + { + code: ` +declare const arr: any[]; +declare const i: number; + +delete arr[i]; + `, + output: ` +declare const arr: any[]; +declare const i: number; + +arr.splice(i, 1); + `, + errors: [{ messageId: 'arrayDeleteViolation' }], + }, + ], +}); From f397a334fd5f682fae3ddaf2032c519c4670e581 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Thu, 26 Jan 2023 20:01:48 +0330 Subject: [PATCH 10/22] remove unneeded files from other branch --- .../docs/rules/call-super-on-override.md | 43 ------- packages/eslint-plugin/src/configs/all.ts | 1 - .../src/rules/call-super-on-override.ts | 119 ----------------- packages/eslint-plugin/src/rules/index.ts | 2 - .../rules/call-super-on-override.test.ts | 121 ------------------ 5 files changed, 286 deletions(-) delete mode 100644 packages/eslint-plugin/docs/rules/call-super-on-override.md delete mode 100644 packages/eslint-plugin/src/rules/call-super-on-override.ts delete mode 100644 packages/eslint-plugin/tests/rules/call-super-on-override.test.ts diff --git a/packages/eslint-plugin/docs/rules/call-super-on-override.md b/packages/eslint-plugin/docs/rules/call-super-on-override.md deleted file mode 100644 index de56a8455e30..000000000000 --- a/packages/eslint-plugin/docs/rules/call-super-on-override.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: 'Require overridden methods to call super.method in their body.' ---- - -> 🛑 This file is source code, not the primary documentation location! 🛑 -> -> See **https://typescript-eslint.io/rules/call-super-on-override** for documentation. - -This rule enforces that overridden methods are calling exact super method to avoid missing super class method implementations. - -## Rule Details - -Examples of code for this rule: - -### ❌ Incorrect - -```ts -class Foo1 { - bar(param: any): void {} -} - -class Foo2 extends Foo1 { - override bar(param: any): void {} -} -``` - -### ✅ Correct - -```ts -class Foo1 { - bar(param: any): void {} -} - -class Foo2 extends Foo1 { - override bar(param: any): void { - super.bar(param); - } -} -``` - -## When Not To Use It - -When you are using TypeScript < 4.3 or you did not set `noImplicitOverride: true` in `CompilerOptions` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index e27aef74315c..fb642f62d7e3 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -13,7 +13,6 @@ export = { '@typescript-eslint/ban-types': 'error', 'brace-style': 'off', '@typescript-eslint/brace-style': 'error', - '@typescript-eslint/call-super-on-override': 'error', '@typescript-eslint/class-literal-property-style': 'error', 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': 'error', diff --git a/packages/eslint-plugin/src/rules/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts deleted file mode 100644 index 29100a3f967a..000000000000 --- a/packages/eslint-plugin/src/rules/call-super-on-override.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; - -import * as utils from '../util'; - -type MessageIds = 'missingSuperMethodCall'; - -export default utils.createRule<[], MessageIds>({ - name: 'call-super-on-override', - meta: { - type: 'suggestion', - docs: { - description: - 'Require overridden methods to call super.method in their body', - recommended: false, - requiresTypeChecking: false, - }, - messages: { - missingSuperMethodCall: - "Use 'super{{property}}{{parameterTuple}}' to avoid missing super class method implementations", - }, - fixable: 'code', - schema: [ - { - type: 'object', - properties: { - topLevel: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [], - create(context) { - return { - 'MethodDefinition[override=true][kind="method"]'( - node: TSESTree.MethodDefinition, - ): void { - let methodName = '', - methodNameIsLiteral = false, - methodNameIsNull = false; // don't add quotes for error message on [null] case - - if (node.key.type === AST_NODE_TYPES.Identifier) { - methodName = node.key.name; - } else { - methodNameIsLiteral = true; - // null & undefined can be used as property names, undefined counted as Identifier & null as Literal - methodName = - (node.key as TSESTree.Literal).value?.toString() ?? 'null'; - methodNameIsNull = (node.key as TSESTree.Literal).value == null; - } - - const { computed: isComputed } = node, - bodyStatements = node.value.body!.body; - - // Search for super method call - for (const statement of bodyStatements) { - if ( - isSuperMethodCall( - statement, - methodName, - !methodNameIsLiteral && isComputed, - ) - ) { - return; // We are done here, no missingSuperMethodCall error - } - } - - // Raise if not found - context.report({ - messageId: 'missingSuperMethodCall', - node: node, - data: { - property: isComputed - ? `[${ - methodNameIsLiteral && !methodNameIsNull - ? `'${methodName}'` - : methodName - }]` - : `.${methodName}`, - parameterTuple: `(${node.value.params - .map(p => (p as TSESTree.Identifier).name) - .join(', ')})`, - }, - }); - }, - }; - }, -}); - -const isSuperMethodCall = ( - statement: TSESTree.Statement | undefined, - methodName: string, - methodIsComputedIdentifier: boolean, -): boolean => { - // for edge cases like this -> override [X]() { super.X() } - // we make sure that computed identifier should have computed callback - let calleeIsComputedIdentifier = false; - - const calleeName = - statement?.type === AST_NODE_TYPES.ExpressionStatement && - statement.expression.type === AST_NODE_TYPES.CallExpression && - statement.expression.callee.type === AST_NODE_TYPES.MemberExpression && - statement.expression.callee.object.type === AST_NODE_TYPES.Super && - (statement.expression.callee.property.type === AST_NODE_TYPES.Identifier - ? ((calleeIsComputedIdentifier = statement.expression.callee.computed), - statement.expression.callee.property.name) - : statement.expression.callee.property.type === AST_NODE_TYPES.Literal - ? statement.expression.callee.property.value?.toString() ?? 'null' - : undefined); - - return methodIsComputedIdentifier - ? calleeIsComputedIdentifier - ? methodName === calleeName - : false - : methodName === calleeName; -}; diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 999eef7cce83..2eb4a404431b 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -5,7 +5,6 @@ import banTsComment from './ban-ts-comment'; import banTslintComment from './ban-tslint-comment'; import banTypes from './ban-types'; import braceStyle from './brace-style'; -import callSuperOnOverride from './call-super-on-override'; import classLiteralPropertyStyle from './class-literal-property-style'; import commaDangle from './comma-dangle'; import commaSpacing from './comma-spacing'; @@ -139,7 +138,6 @@ export default { 'ban-tslint-comment': banTslintComment, 'ban-types': banTypes, 'brace-style': braceStyle, - 'call-super-on-override': callSuperOnOverride, 'class-literal-property-style': classLiteralPropertyStyle, 'comma-dangle': commaDangle, 'comma-spacing': commaSpacing, diff --git a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts deleted file mode 100644 index 03ab3ee02ece..000000000000 --- a/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import rule from '../../src/rules/call-super-on-override'; -import { RuleTester } from '../RuleTester'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); - -ruleTester.run('call-super-on-override', rule, { - valid: [ - { - code: ` -class ValidSample { - override x() { - this.y(); - super.x(); - } -} - `, - }, - { - code: ` -class ValidSample { - override ['x-y']() { - super['x-y'](); - } - override ['z']() { - super.z(); - } - override h() { - super['h'](); - } - override [M]() { - super[M](); - } -} - `, - }, - ], - invalid: [ - { - code: ` -class InvalidSample { - override x() { - this.x(); - super.x = () => void 0; - super.x; - } -} - `, - errors: [ - { - messageId: 'missingSuperMethodCall', - data: { property: '.x', parameterTuple: '()' }, - }, - ], - }, - { - code: ` -class InvalidSample { - override ['x-y-z']() { - this['x-y-z'](); - super['x-y-z'] = () => void 0; - super['x-y-z']; - } -} - `, - errors: [ - { - messageId: 'missingSuperMethodCall', - data: { property: "['x-y-z']", parameterTuple: '()' }, - }, - ], - }, - { - code: ` -class InvalidSample { - override x(y: number, z: string) {} -} - `, - errors: [ - { - messageId: 'missingSuperMethodCall', - data: { property: '.x', parameterTuple: '(y, z)' }, - }, - ], - }, - { - code: ` -class InvalidSample { - override [M]() { - super.M(); - } -} - `, - errors: [ - { - messageId: 'missingSuperMethodCall', - data: { property: '[M]', parameterTuple: '()' }, - }, - ], - }, - { - code: ` -class InvalidSample { - override [null]() {} - override ['null']() {} -} - `, - errors: [ - { - messageId: 'missingSuperMethodCall', - data: { property: '[null]', parameterTuple: '()' }, - }, - { - messageId: 'missingSuperMethodCall', - data: { property: "['null']", parameterTuple: '()' }, - }, - ], - }, - ], -}); From c32bb0a62d2dbc6eeb5e44ac2bb5101f5af14c17 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Thu, 26 Jan 2023 20:04:18 +0330 Subject: [PATCH 11/22] remove unneeded files from other branch --- .../eslint-plugin/docs/rules/no-in-array.md | 37 --------------- packages/eslint-plugin/src/configs/all.ts | 1 - .../recommended-requiring-type-checking.ts | 1 - packages/eslint-plugin/src/rules/index.ts | 2 - .../eslint-plugin/src/rules/no-in-array.ts | 46 ------------------- .../tests/rules/no-in-array.test.ts | 40 ---------------- 6 files changed, 127 deletions(-) delete mode 100644 packages/eslint-plugin/docs/rules/no-in-array.md delete mode 100644 packages/eslint-plugin/src/rules/no-in-array.ts delete mode 100644 packages/eslint-plugin/tests/rules/no-in-array.test.ts diff --git a/packages/eslint-plugin/docs/rules/no-in-array.md b/packages/eslint-plugin/docs/rules/no-in-array.md deleted file mode 100644 index c8f866647d69..000000000000 --- a/packages/eslint-plugin/docs/rules/no-in-array.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -description: 'Disallow using in operator for arrays.' ---- - -> 🛑 This file is source code, not the primary documentation location! 🛑 -> -> See **https://typescript-eslint.io/rules/no-in-array** for documentation. - -This rule bans using `in` operator for checking array members existence. - -## Rule Details - -Examples of code for this rule: - -### ❌ Incorrect - -```ts -const arr = ['a', 'b', 'c']; - -if ('c' in arr) { - // ... -} -``` - -### ✅ Correct - -```ts -const arr = ['a', 'b', 'c']; - -if (arr.includes('a')) { - // ... -} -``` - -## When Not To Use It - -When you want exactly iterate over array indexes. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index fb642f62d7e3..ec41089a9c08 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -70,7 +70,6 @@ export = { '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', - '@typescript-eslint/no-in-array': 'error', '@typescript-eslint/no-inferrable-types': 'error', 'no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts index d8e260a1800b..9f6f5455e31f 100644 --- a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts +++ b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts @@ -11,7 +11,6 @@ export = { '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', - '@typescript-eslint/no-in-array': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unsafe-argument': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2eb4a404431b..318f3264cc3d 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -48,7 +48,6 @@ import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; import noImplicitAnyCatch from './no-implicit-any-catch'; import noImpliedEval from './no-implied-eval'; -import noInArray from './no-in-array'; import noInferrableTypes from './no-inferrable-types'; import noInvalidThis from './no-invalid-this'; import noInvalidVoidType from './no-invalid-void-type'; @@ -181,7 +180,6 @@ export default { 'no-for-in-array': noForInArray, 'no-implicit-any-catch': noImplicitAnyCatch, 'no-implied-eval': noImpliedEval, - 'no-in-array': noInArray, 'no-inferrable-types': noInferrableTypes, 'no-invalid-this': noInvalidThis, 'no-invalid-void-type': noInvalidVoidType, diff --git a/packages/eslint-plugin/src/rules/no-in-array.ts b/packages/eslint-plugin/src/rules/no-in-array.ts deleted file mode 100644 index 7f269c0377b6..000000000000 --- a/packages/eslint-plugin/src/rules/no-in-array.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TSESTree } from '@typescript-eslint/utils'; -import * as ts from 'typescript'; - -import * as util from '../util'; - -export default util.createRule<[], 'inArrayViolation'>({ - name: 'no-in-array', - meta: { - docs: { - description: 'Disallow using in operator for arrays', - recommended: 'error', - requiresTypeChecking: true, - }, - messages: { - inArrayViolation: - "'in' operator for arrays is forbidden. Use array.indexOf or array.includes instead.", - }, - schema: [], - type: 'problem', - }, - defaultOptions: [], - create(context) { - return { - "BinaryExpression[operator='in']"(node: TSESTree.BinaryExpression): void { - const parserServices = util.getParserServices(context); - const checker = parserServices.program.getTypeChecker(); - const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); - - const type = util.getConstrainedTypeAtLocation( - checker, - originalNode.right, - ); - - if ( - util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker) || - (type.flags & ts.TypeFlags.StringLike) !== 0 - ) { - context.report({ - node, - messageId: 'inArrayViolation', - }); - } - }, - }; - }, -}); diff --git a/packages/eslint-plugin/tests/rules/no-in-array.test.ts b/packages/eslint-plugin/tests/rules/no-in-array.test.ts deleted file mode 100644 index 984bc9bd7755..000000000000 --- a/packages/eslint-plugin/tests/rules/no-in-array.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import rule from '../../src/rules/no-in-array'; -import { getFixturesRootDir, RuleTester } from '../RuleTester'; - -const rootDir = getFixturesRootDir(); -const ruleTester = new RuleTester({ - parserOptions: { - ecmaVersion: 2015, - tsconfigRootDir: rootDir, - project: './tsconfig.json', - }, - parser: '@typescript-eslint/parser', -}); - -ruleTester.run('no-in-array', rule, { - valid: [ - { - code: ` -if (x in {}) { -} - `, - }, - ], - invalid: [ - { - code: ` -if (x in ['z', 'y']) { -} - `, - errors: [{ messageId: 'inArrayViolation' }], - }, - { - code: ` -const arr = [5, 6, 7, 8]; - -const has_6 = 6 in arr; - `, - errors: [{ messageId: 'inArrayViolation' }], - }, - ], -}); From 0b59dc68f8f07bd08e3c6f31aeb1e8652a286d06 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Sun, 26 Feb 2023 14:48:56 +0330 Subject: [PATCH 12/22] Apply Suggested Changes --- .../docs/rules/no-array-delete.md | 8 +- .../src/rules/no-array-delete.ts | 73 +++-- .../tests/rules/no-array-delete.test.ts | 281 +++++++++++++++++- 3 files changed, 328 insertions(+), 34 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-array-delete.md b/packages/eslint-plugin/docs/rules/no-array-delete.md index aa13918af9ab..1e09bbd1df74 100644 --- a/packages/eslint-plugin/docs/rules/no-array-delete.md +++ b/packages/eslint-plugin/docs/rules/no-array-delete.md @@ -6,8 +6,8 @@ description: 'Disallow delete operator for arrays.' > > See **https://typescript-eslint.io/rules/no-array-delete** for documentation. -In JavaScript `delete` operator in arrays makes the given index empty, and leaves the array length as is. this may -cause a problem for optimizing and undefined behaviors like getting `undefined` when accessing that index. +In JavaScript `delete` operator in arrays makes the given index empty, and leaves the array length unchanged. +This can sometimes cause problems with performance and unexpected behaviors around array loops and index accesses. ## Examples @@ -16,7 +16,7 @@ cause a problem for optimizing and undefined behaviors like getting `undefined` ### ❌ Incorrect ```ts -declare const array: any[]; +declare const array: unknown[]; declare const index: number; delete array[index]; @@ -25,7 +25,7 @@ delete array[index]; ### ✅ Correct ```js -declare const array: any[]; +declare const array: unknown[]; declare const index: number; array.splice(index, 1); diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index 4a6b6d7eb6b7..65bfa49a8189 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -1,11 +1,13 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { unionTypeParts } from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; -export default util.createRule<[], 'arrayDeleteViolation'>({ +export default util.createRule({ name: 'no-array-delete', meta: { + hasSuggestions: true, docs: { description: 'Disallow delete operator for arrays', recommended: 'error', @@ -13,7 +15,7 @@ export default util.createRule<[], 'arrayDeleteViolation'>({ }, messages: { arrayDeleteViolation: - 'Using delete operator for arrays are forbidden. Use array.splice instead.', + 'Using the delete operator on an array is dangerous.', }, schema: [], type: 'problem', @@ -22,36 +24,67 @@ export default util.createRule<[], 'arrayDeleteViolation'>({ defaultOptions: [], create(context) { return { - "UnaryExpression[operator='delete']"( - node: TSESTree.UnaryExpression, + "UnaryExpression[operator='delete'] > MemberExpression[computed]"( + node: TSESTree.MemberExpressionComputedName & { + parent: TSESTree.UnaryExpression; + }, ): void { const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const expression = originalNode.getChildAt(1); - const target = expression.getChildAt(0), - key = expression.getChildAt(2); + const target = originalNode.getChildAt(0), + key = originalNode.getChildAt(2); const targetType = util.getConstrainedTypeAtLocation(checker, target), keyType = util.getConstrainedTypeAtLocation(checker, key); if ( - util.isTypeAnyArrayType(targetType, checker) && - (keyType.flags & ts.TypeFlags.Number) !== 0 + !isTypeArrayTypeOrArrayInUnionOfTypes(targetType, checker) || + !isTypeNumberOrNumberLiteralOrNumberLikeType(keyType) ) { - context.report({ - node, - messageId: 'arrayDeleteViolation', - fix(fixer) { - return fixer.replaceText( - node, - `${target.getText()}.splice(${key.getText()}, 1)`, - ); - }, - }); + return; } + + context.report({ + node, + messageId: 'arrayDeleteViolation', + suggest: [ + { + messageId: 'arrayDeleteViolation', + fix(fixer): TSESLint.RuleFix { + return fixer.replaceText( + node.parent, + `${target.getText()}.splice(${key.getText()}, 1)`, + ); + }, + }, + ], + }); }, }; }, }); + +function isTypeArrayTypeOrArrayInUnionOfTypes( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + for (const t of unionTypeParts(type)) { + if (checker.isArrayType(t)) { + return true; + } + } + + return false; +} + +function isTypeNumberOrNumberLiteralOrNumberLikeType(type: ts.Type): boolean { + return ( + (type.flags & + (ts.TypeFlags.Number | + ts.TypeFlags.NumberLiteral | + ts.TypeFlags.NumberLike)) !== + 0 + ); +} diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index 6d39e56b0a1c..3d6abbb06c2e 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -14,38 +14,299 @@ const ruleTester = new RuleTester({ ruleTester.run('no-array-delete', rule, { valid: [ ` -declare const obj: Record; +declare const obj: Record; declare const key: PropertyKey; - delete obj[key]; `, ` -declare const arr: any[]; - +declare const arr: unknown[]; delete arr.myprop; `, ` -declare const arr: any[]; +declare const arr: unknown[]; declare const i: string; - delete arr[i]; `, + ` +declare const multiDimesnional: Array[][][][]; +declare const i: number; +delete multiDimesnional[i][i][i][i][i].someProp; + `, ], invalid: [ { code: ` -declare const arr: any[]; +declare const arr: unknown[]; +declare const i: number; + +delete arr[i]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const arr: unknown[]; +declare const i: number; + +arr.splice(i, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: unknown[]; + +delete arr[10]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const arr: unknown[]; + +arr.splice(10, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: unknown[]; + +enum Enum { + X, + Y, +} + +delete arr[Enum.X]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const arr: unknown[]; + +enum Enum { + X, + Y, +} + +arr.splice(Enum.X, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: Array; declare const i: number; delete arr[i]; `, - output: ` -declare const arr: any[]; + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const arr: Array; declare const i: number; arr.splice(i, 1); `, - errors: [{ messageId: 'arrayDeleteViolation' }], + }, + ], + }, + ], + }, + { + code: ` +declare const obj: { prop: { arr: unknown[] } }; +declare const indexObj: { i: number }; + +delete obj.prop.arr[indexObj.i]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const obj: { prop: { arr: unknown[] } }; +declare const indexObj: { i: number }; + +obj.prop.arr.splice(indexObj.i, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const i: number; +declare function getTarget(): unknown[]; + +delete getTarget()[i]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const i: number; +declare function getTarget(): unknown[]; + +getTarget().splice(i, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const data: unknown[]; +declare function getKey(): number; + +delete data[getKey()]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const data: unknown[]; +declare function getKey(): number; + +data.splice(getKey(), 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const mayBeArr: number | number[]; +declare const i: number; + +delete mayBeArr[i]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const mayBeArr: number | number[]; +declare const i: number; + +mayBeArr.splice(i, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const multiDimesnional: Array[][][][]; +declare const i: number; + +delete multiDimesnional[i][i][i][i][i]; + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const multiDimesnional: Array[][][][]; +declare const i: number; + +multiDimesnional[i][i][i][i].splice(i, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const i: number; + +function trickyCase(t: T) { + delete t[i]; +} + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const i: number; + +function trickyCase(t: T) { + t.splice(i, 1); +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const i: number; + +function trickyCase1(t: T[]) { + delete t[i]; +} + `, + errors: [ + { + messageId: 'arrayDeleteViolation', + suggestions: [ + { + messageId: 'arrayDeleteViolation', + output: ` +declare const i: number; + +function trickyCase1(t: T[]) { + t.splice(i, 1); +} + `, + }, + ], + }, + ], }, ], }); From b7d976c3a1d07b768e1387090fc4055a43495b31 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Tue, 14 Mar 2023 20:38:24 +0330 Subject: [PATCH 13/22] apply suggested changes meta & using built-in functions & consider new rule in code --- .../src/rules/no-array-delete.ts | 39 +++++++--------- .../tests/rules/no-array-delete.test.ts | 44 +++++++++---------- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index 65bfa49a8189..f095a1e0fb2b 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -10,12 +10,11 @@ export default util.createRule({ hasSuggestions: true, docs: { description: 'Disallow delete operator for arrays', - recommended: 'error', + recommended: 'strict', requiresTypeChecking: true, }, messages: { - arrayDeleteViolation: - 'Using the delete operator on an array is dangerous.', + arrayDelete: 'Using the delete operator on an array is dangerous.', }, schema: [], type: 'problem', @@ -33,11 +32,16 @@ export default util.createRule({ const checker = parserServices.program.getTypeChecker(); const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const target = originalNode.getChildAt(0), - key = originalNode.getChildAt(2); + const target = originalNode.getChildAt(0); + const key = originalNode.getChildAt(2); - const targetType = util.getConstrainedTypeAtLocation(checker, target), - keyType = util.getConstrainedTypeAtLocation(checker, key); + const targetType = util.getConstrainedTypeAtLocation(checker, target); + + if (!util.isTypeAnyArrayType) { + return; + } + + const keyType = util.getConstrainedTypeAtLocation(checker, key); if ( !isTypeArrayTypeOrArrayInUnionOfTypes(targetType, checker) || @@ -48,10 +52,10 @@ export default util.createRule({ context.report({ node, - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggest: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', fix(fixer): TSESLint.RuleFix { return fixer.replaceText( node.parent, @@ -70,21 +74,12 @@ function isTypeArrayTypeOrArrayInUnionOfTypes( type: ts.Type, checker: ts.TypeChecker, ): boolean { - for (const t of unionTypeParts(type)) { - if (checker.isArrayType(t)) { - return true; - } - } - - return false; + return unionTypeParts(type).some(checker.isArrayType); } function isTypeNumberOrNumberLiteralOrNumberLikeType(type: ts.Type): boolean { - return ( - (type.flags & - (ts.TypeFlags.Number | - ts.TypeFlags.NumberLiteral | - ts.TypeFlags.NumberLike)) !== - 0 + return util.isTypeFlagSet( + type, + ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral | ts.TypeFlags.NumberLike, ); } diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index 3d6abbb06c2e..bde66376c82a 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -43,10 +43,10 @@ delete arr[i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const arr: unknown[]; declare const i: number; @@ -66,10 +66,10 @@ delete arr[10]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const arr: unknown[]; @@ -93,10 +93,10 @@ delete arr[Enum.X]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const arr: unknown[]; @@ -121,10 +121,10 @@ delete arr[i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const arr: Array; declare const i: number; @@ -145,10 +145,10 @@ delete obj.prop.arr[indexObj.i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const obj: { prop: { arr: unknown[] } }; declare const indexObj: { i: number }; @@ -169,10 +169,10 @@ delete getTarget()[i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const i: number; declare function getTarget(): unknown[]; @@ -193,10 +193,10 @@ delete data[getKey()]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const data: unknown[]; declare function getKey(): number; @@ -217,10 +217,10 @@ delete mayBeArr[i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const mayBeArr: number | number[]; declare const i: number; @@ -241,10 +241,10 @@ delete multiDimesnional[i][i][i][i][i]; `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const multiDimesnional: Array[][][][]; declare const i: number; @@ -266,10 +266,10 @@ function trickyCase(t: T) { `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const i: number; @@ -292,10 +292,10 @@ function trickyCase1(t: T[]) { `, errors: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDeleteViolation', + messageId: 'arrayDelete', output: ` declare const i: number; From ab27b880419446e36df4f8b530fe7a5534918d40 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Tue, 14 Mar 2023 21:04:51 +0330 Subject: [PATCH 14/22] more test & fix sequence expr suggestions --- .../src/rules/no-array-delete.ts | 10 ++- .../tests/rules/no-array-delete.test.ts | 64 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index f095a1e0fb2b..e4927dcb878c 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -1,4 +1,4 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'; import { unionTypeParts } from 'tsutils'; import * as ts from 'typescript'; @@ -57,9 +57,15 @@ export default util.createRule({ { messageId: 'arrayDelete', fix(fixer): TSESLint.RuleFix { + const requiresParens = + node.property.type === AST_NODE_TYPES.SequenceExpression; + const keyText = key.getText(); + return fixer.replaceText( node.parent, - `${target.getText()}.splice(${key.getText()}, 1)`, + `${target.getText()}.splice(${ + requiresParens ? `(${keyText})` : keyText + }, 1)`, ); }, }, diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index bde66376c82a..d73fbed41d5e 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -308,5 +308,69 @@ function trickyCase1(t: T[]) { }, ], }, + { + code: ` +declare const arr: unknown[]; +delete arr[Math.random() ? 1 : 1]; + `, + errors: [ + { + messageId: 'arrayDelete', + suggestions: [ + { + messageId: 'arrayDelete', + output: ` +declare const arr: unknown[]; +arr.splice(Math.random() ? 1 : 1, 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: unknown[]; +delete arr[Math.random() ? 1 : "prop"]; + `, + errors: [ + { + messageId: 'arrayDelete', + suggestions: [ + { + messageId: 'arrayDelete', + output: ` +declare const arr: unknown[]; +arr.splice(Math.random() ? 1 : "prop", 1); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: unknown[]; +declare function something(): unknown; + +delete arr[something(), 1]; + `, + errors: [ + { + messageId: 'arrayDelete', + suggestions: [ + { + messageId: 'arrayDelete', + output: ` +declare const arr: unknown[]; +declare function something(): unknown; + +arr.splice((something(), 1), 1); + `, + }, + ], + }, + ], + }, ], }); From e4689f0fb55d17363063eb2979b7abb588dff654 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Thu, 16 Mar 2023 23:55:14 +0330 Subject: [PATCH 15/22] Fix Unit Test & Lint Test Errors --- .../src/configs/recommended-requiring-type-checking.ts | 1 - packages/eslint-plugin/src/configs/strict.ts | 1 + packages/eslint-plugin/src/rules/no-array-delete.ts | 3 ++- packages/eslint-plugin/tests/rules/no-array-delete.test.ts | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts index 9f6f5455e31f..369d33d6687e 100644 --- a/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts +++ b/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts @@ -6,7 +6,6 @@ export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-array-delete': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index c63b4173452b..5622f63aaaf2 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -14,6 +14,7 @@ export = { '@typescript-eslint/consistent-type-definitions': 'warn', 'dot-notation': 'off', '@typescript-eslint/dot-notation': 'warn', + '@typescript-eslint/no-array-delete': 'warn', '@typescript-eslint/no-base-to-string': 'warn', '@typescript-eslint/no-confusing-non-null-assertion': 'warn', '@typescript-eslint/no-duplicate-enum-values': 'warn', diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index e4927dcb878c..39874b2c4438 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -1,4 +1,5 @@ -import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { unionTypeParts } from 'tsutils'; import * as ts from 'typescript'; diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index d73fbed41d5e..319d9a6f857b 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -312,7 +312,7 @@ function trickyCase1(t: T[]) { code: ` declare const arr: unknown[]; delete arr[Math.random() ? 1 : 1]; - `, + `, errors: [ { messageId: 'arrayDelete', @@ -332,7 +332,7 @@ arr.splice(Math.random() ? 1 : 1, 1); code: ` declare const arr: unknown[]; delete arr[Math.random() ? 1 : "prop"]; - `, + `, errors: [ { messageId: 'arrayDelete', @@ -354,7 +354,7 @@ declare const arr: unknown[]; declare function something(): unknown; delete arr[something(), 1]; - `, + `, errors: [ { messageId: 'arrayDelete', From 7f3a68daa1cb4254d5a2fc8013b16782c99d1152 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Fri, 17 Mar 2023 11:48:46 +0330 Subject: [PATCH 16/22] Fix spacing causing unit test error --- packages/eslint-plugin/tests/rules/no-array-delete.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index 319d9a6f857b..51b9aa14efc0 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -322,7 +322,7 @@ delete arr[Math.random() ? 1 : 1]; output: ` declare const arr: unknown[]; arr.splice(Math.random() ? 1 : 1, 1); - `, + `, }, ], }, @@ -342,7 +342,7 @@ delete arr[Math.random() ? 1 : "prop"]; output: ` declare const arr: unknown[]; arr.splice(Math.random() ? 1 : "prop", 1); - `, + `, }, ], }, @@ -366,7 +366,7 @@ declare const arr: unknown[]; declare function something(): unknown; arr.splice((something(), 1), 1); - `, + `, }, ], }, From de0a0b812ae7e3310055ebc74b222b9297c74265 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:06:43 +0330 Subject: [PATCH 17/22] fix linting problem in test file --- .../eslint-plugin/tests/rules/no-array-delete.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index 51b9aa14efc0..a04899386752 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -312,7 +312,7 @@ function trickyCase1(t: T[]) { code: ` declare const arr: unknown[]; delete arr[Math.random() ? 1 : 1]; - `, + `, errors: [ { messageId: 'arrayDelete', @@ -331,8 +331,8 @@ arr.splice(Math.random() ? 1 : 1, 1); { code: ` declare const arr: unknown[]; -delete arr[Math.random() ? 1 : "prop"]; - `, +delete arr[Math.random() ? 1 : 'prop']; + `, errors: [ { messageId: 'arrayDelete', @@ -353,8 +353,8 @@ arr.splice(Math.random() ? 1 : "prop", 1); declare const arr: unknown[]; declare function something(): unknown; -delete arr[something(), 1]; - `, +delete arr[(something(), 1)]; + `, errors: [ { messageId: 'arrayDelete', From a3290eca307ecca45a1283decb1d9baf81dc3c09 Mon Sep 17 00:00:00 2001 From: SoR <65429393+mahdi-farnia@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:16:14 +0330 Subject: [PATCH 18/22] reduce amount of type request by split if conditions Co-authored-by: Armano --- packages/eslint-plugin/src/rules/no-array-delete.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index 39874b2c4438..e352788a7af3 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -42,12 +42,13 @@ export default util.createRule({ return; } + if (!isTypeArrayTypeOrArrayInUnionOfTypes(targetType, checker)) { + return; + } + const keyType = util.getConstrainedTypeAtLocation(checker, key); - if ( - !isTypeArrayTypeOrArrayInUnionOfTypes(targetType, checker) || - !isTypeNumberOrNumberLiteralOrNumberLikeType(keyType) - ) { + if (!isTypeNumberOrNumberLiteralOrNumberLikeType(keyType)) { return; } From b5d99e03f928a57a0eebe68b64dc430204f48f10 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:27:51 +0330 Subject: [PATCH 19/22] apply suggested changes remove extra if statement --- packages/eslint-plugin/src/rules/no-array-delete.ts | 9 +++------ .../eslint-plugin/tests/rules/no-array-delete.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index e352788a7af3..e32490aacfd4 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -23,14 +23,15 @@ export default util.createRule({ }, defaultOptions: [], create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + return { "UnaryExpression[operator='delete'] > MemberExpression[computed]"( node: TSESTree.MemberExpressionComputedName & { parent: TSESTree.UnaryExpression; }, ): void { - const parserServices = util.getParserServices(context); - const checker = parserServices.program.getTypeChecker(); const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const target = originalNode.getChildAt(0); @@ -38,10 +39,6 @@ export default util.createRule({ const targetType = util.getConstrainedTypeAtLocation(checker, target); - if (!util.isTypeAnyArrayType) { - return; - } - if (!isTypeArrayTypeOrArrayInUnionOfTypes(targetType, checker)) { return; } diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index a04899386752..c94d17121f8b 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -322,7 +322,7 @@ delete arr[Math.random() ? 1 : 1]; output: ` declare const arr: unknown[]; arr.splice(Math.random() ? 1 : 1, 1); - `, + `, }, ], }, @@ -341,8 +341,8 @@ delete arr[Math.random() ? 1 : 'prop']; messageId: 'arrayDelete', output: ` declare const arr: unknown[]; -arr.splice(Math.random() ? 1 : "prop", 1); - `, +arr.splice(Math.random() ? 1 : 'prop', 1); + `, }, ], }, @@ -365,8 +365,8 @@ delete arr[(something(), 1)]; declare const arr: unknown[]; declare function something(): unknown; -arr.splice((something(), 1), 1); - `, +arr.splice(((something(), 1)), 1); + `, }, ], }, From 8230f09cafc240b713dff1aaaaeec06d62a76c16 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 6 May 2023 15:23:42 +0930 Subject: [PATCH 20/22] Update no-array-delete.md --- packages/eslint-plugin/docs/rules/no-array-delete.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-array-delete.md b/packages/eslint-plugin/docs/rules/no-array-delete.md index 1e09bbd1df74..8761a65532e3 100644 --- a/packages/eslint-plugin/docs/rules/no-array-delete.md +++ b/packages/eslint-plugin/docs/rules/no-array-delete.md @@ -24,7 +24,7 @@ delete array[index]; ### ✅ Correct -```js +```ts declare const array: unknown[]; declare const index: number; From 2392654792710ad151c216708daca8e29690d4b1 Mon Sep 17 00:00:00 2001 From: SoR <65429393+mahdi-farnia@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:06:58 +0330 Subject: [PATCH 21/22] make doc more clearer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/docs/rules/no-array-delete.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-array-delete.md b/packages/eslint-plugin/docs/rules/no-array-delete.md index 8761a65532e3..2e852d71f602 100644 --- a/packages/eslint-plugin/docs/rules/no-array-delete.md +++ b/packages/eslint-plugin/docs/rules/no-array-delete.md @@ -6,7 +6,8 @@ description: 'Disallow delete operator for arrays.' > > See **https://typescript-eslint.io/rules/no-array-delete** for documentation. -In JavaScript `delete` operator in arrays makes the given index empty, and leaves the array length unchanged. +In JavaScript, using the `delete` operator on an array makes the given index empty and leaves the array length unchanged. +See [MDN's _Deleting array elements_ documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#deleting_array_elements) for more information. This can sometimes cause problems with performance and unexpected behaviors around array loops and index accesses. ## Examples From 04ec03132916e8e176156586d26eb98096cb2097 Mon Sep 17 00:00:00 2001 From: mahdi-farnia <65429393+mahdi-farnia@users.noreply.github.com> Date: Sun, 30 Jul 2023 22:30:57 +0330 Subject: [PATCH 22/22] fix(no-array-delete): buggy suggestion apply suggested changes --- .../docs/rules/no-array-delete.md | 4 ++ .../src/rules/no-array-delete.ts | 23 +++++----- .../tests/rules/no-array-delete.test.ts | 42 ++++++++++++------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-array-delete.md b/packages/eslint-plugin/docs/rules/no-array-delete.md index 2e852d71f602..8497a7edff73 100644 --- a/packages/eslint-plugin/docs/rules/no-array-delete.md +++ b/packages/eslint-plugin/docs/rules/no-array-delete.md @@ -31,3 +31,7 @@ declare const index: number; array.splice(index, 1); ``` + +## When Not To Use It + +If you don't care about having empty element in array, then you will not need this rule. diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index e32490aacfd4..ac42edb232cb 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -5,7 +5,9 @@ import * as ts from 'typescript'; import * as util from '../util'; -export default util.createRule({ +type MessageIds = 'arrayDelete' | 'suggestFunctionalDelete'; + +export default util.createRule<[], MessageIds>({ name: 'no-array-delete', meta: { hasSuggestions: true, @@ -16,6 +18,8 @@ export default util.createRule({ }, messages: { arrayDelete: 'Using the delete operator on an array is dangerous.', + suggestFunctionalDelete: + 'Using Array.slice instead of delete keyword prevents empty array element.', }, schema: [], type: 'problem', @@ -45,7 +49,7 @@ export default util.createRule({ const keyType = util.getConstrainedTypeAtLocation(checker, key); - if (!isTypeNumberOrNumberLiteralOrNumberLikeType(keyType)) { + if (!util.isTypeFlagSet(keyType, ts.TypeFlags.NumberLike)) { return; } @@ -54,12 +58,16 @@ export default util.createRule({ messageId: 'arrayDelete', suggest: [ { - messageId: 'arrayDelete', - fix(fixer): TSESLint.RuleFix { + messageId: 'suggestFunctionalDelete', + fix(fixer): TSESLint.RuleFix | null { const requiresParens = node.property.type === AST_NODE_TYPES.SequenceExpression; const keyText = key.getText(); + if (util.isTypeFlagSet(keyType, ts.TypeFlags.String)) { + return null; + } + return fixer.replaceText( node.parent, `${target.getText()}.splice(${ @@ -81,10 +89,3 @@ function isTypeArrayTypeOrArrayInUnionOfTypes( ): boolean { return unionTypeParts(type).some(checker.isArrayType); } - -function isTypeNumberOrNumberLiteralOrNumberLikeType(type: ts.Type): boolean { - return util.isTypeFlagSet( - type, - ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral | ts.TypeFlags.NumberLike, - ); -} diff --git a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts index c94d17121f8b..6a9c8ed82f52 100644 --- a/packages/eslint-plugin/tests/rules/no-array-delete.test.ts +++ b/packages/eslint-plugin/tests/rules/no-array-delete.test.ts @@ -46,7 +46,7 @@ delete arr[i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; declare const i: number; @@ -69,7 +69,7 @@ delete arr[10]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; @@ -96,7 +96,7 @@ delete arr[Enum.X]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; @@ -124,7 +124,7 @@ delete arr[i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: Array; declare const i: number; @@ -148,7 +148,7 @@ delete obj.prop.arr[indexObj.i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const obj: { prop: { arr: unknown[] } }; declare const indexObj: { i: number }; @@ -172,7 +172,7 @@ delete getTarget()[i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const i: number; declare function getTarget(): unknown[]; @@ -196,7 +196,7 @@ delete data[getKey()]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const data: unknown[]; declare function getKey(): number; @@ -220,7 +220,7 @@ delete mayBeArr[i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const mayBeArr: number | number[]; declare const i: number; @@ -244,7 +244,7 @@ delete multiDimesnional[i][i][i][i][i]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const multiDimesnional: Array[][][][]; declare const i: number; @@ -269,7 +269,7 @@ function trickyCase(t: T) { messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const i: number; @@ -295,7 +295,7 @@ function trickyCase1(t: T[]) { messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const i: number; @@ -318,7 +318,7 @@ delete arr[Math.random() ? 1 : 1]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; arr.splice(Math.random() ? 1 : 1, 1); @@ -338,7 +338,7 @@ delete arr[Math.random() ? 1 : 'prop']; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; arr.splice(Math.random() ? 1 : 'prop', 1); @@ -360,7 +360,7 @@ delete arr[(something(), 1)]; messageId: 'arrayDelete', suggestions: [ { - messageId: 'arrayDelete', + messageId: 'suggestFunctionalDelete', output: ` declare const arr: unknown[]; declare function something(): unknown; @@ -372,5 +372,19 @@ arr.splice(((something(), 1)), 1); }, ], }, + { + code: ` +declare const arr: unknown[]; +declare const i: number | string; + +delete arr[i]; + `, + errors: [ + { + messageId: 'arrayDelete', + suggestions: [], + }, + ], + }, ], });