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..de56a8455e30 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/call-super-on-override.md @@ -0,0 +1,43 @@ +--- +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/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 1f6530ead3ec..ec184f00355b 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', @@ -69,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/call-super-on-override.ts b/packages/eslint-plugin/src/rules/call-super-on-override.ts new file mode 100644 index 000000000000..29100a3f967a --- /dev/null +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -0,0 +1,119 @@ +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 851661400fbb..8daf8fd54744 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'; @@ -47,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'; @@ -135,6 +137,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, @@ -177,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/call-super-on-override.test.ts b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts new file mode 100644 index 000000000000..03ab3ee02ece --- /dev/null +++ b/packages/eslint-plugin/tests/rules/call-super-on-override.test.ts @@ -0,0 +1,121 @@ +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: '()' }, + }, + ], + }, + ], +}); 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' }], + }, + ], +});