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..77a10587befb --- /dev/null +++ b/packages/eslint-plugin/docs/rules/call-super-on-override.md @@ -0,0 +1,40 @@ +--- +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 call their corresponding `super` method. +Doing so can be useful in architectures where base class methods are meant to always be called by child classes. + +## 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); + } +} +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 63ce9b1305a9..6cf8fa3e8119 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -15,6 +15,7 @@ export = { '@typescript-eslint/block-spacing': '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 new file mode 100644 index 000000000000..13414ce43e15 --- /dev/null +++ b/packages/eslint-plugin/src/rules/call-super-on-override.ts @@ -0,0 +1,106 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import * as utils from '../util'; + +/** + * TODO: + * 1. Grabbing the type of the extended class + * 2. Checking whether it has a method / function property under the same name + */ + +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: false, + }, + messages: { + missingSuperMethodCall: + "Use 'super{{property}}{{parameterTuple}}' to avoid missing super class method implementations", + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'MethodDefinition[override=true][kind="method"]'( + node: TSESTree.MethodDefinition, + ): void { + const methodName = + node.key.type === AST_NODE_TYPES.Identifier + ? node.key.name + : (node.key as TSESTree.Literal).value?.toString() ?? 'null'; + const methodNameIsLiteral = node.key.type === AST_NODE_TYPES.Identifier; + const methodNameIsNull = + node.key.type !== AST_NODE_TYPES.Identifier + ? (node.key as TSESTree.Literal).value == null + : false; + + const { computed: isComputed } = node; + const bodyStatements = node.value.body!.body; + + for (const statement of bodyStatements) { + if ( + isSuperMethodCall( + statement, + methodName, + !methodNameIsLiteral && isComputed, + ) + ) { + return; + } + } + + 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 e1d871103ec9..09fdd4a09514 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -6,6 +6,7 @@ import banTslintComment from './ban-tslint-comment'; import banTypes from './ban-types'; import blockSpacing from './block-spacing'; 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'; @@ -142,6 +143,7 @@ export default { 'ban-types': banTypes, 'block-spacing': blockSpacing, '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..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: '()' }, + }, + ], + }, + ], +});