From 04b4d2ca51146e96fbdbba0dc9009ca3d752f812 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 13 Feb 2023 12:06:50 +1030 Subject: [PATCH] feat(eslint-plugin): [class-methods-use-this] add extension rule --- .../eslint-plugin/TSLINT_RULE_ALTERNATIVES.md | 4 +- packages/eslint-plugin/docs/rules/TEMPLATE.md | 6 +- .../docs/rules/class-methods-use-this.md | 51 +++ packages/eslint-plugin/src/configs/all.ts | 2 + .../src/rules/class-methods-use-this.ts | 265 +++++++++++ packages/eslint-plugin/src/rules/index.ts | 2 + .../src/util/getStaticStringValue.ts | 49 ++ packages/eslint-plugin/src/util/index.ts | 1 + .../eslint-plugin/src/util/isNullLiteral.ts | 2 +- .../class-methods-use-this-core.test.ts | 431 ++++++++++++++++++ .../class-methods-use-this.test.ts | 185 ++++++++ 11 files changed, 994 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/class-methods-use-this.md create mode 100644 packages/eslint-plugin/src/rules/class-methods-use-this.ts create mode 100644 packages/eslint-plugin/src/util/getStaticStringValue.ts create mode 100644 packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts create mode 100644 packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts diff --git a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md index 097de8938048..8276fabd9226 100644 --- a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md +++ b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md @@ -185,7 +185,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th | [`one-line`] | 🌟 | [`brace-style`][brace-style] or [Prettier] | | [`one-variable-per-declaration`] | 🌟 | [`one-var`][one-var] | | [`ordered-imports`] | 🌓 | [`import/order`] | -| [`prefer-function-over-method`] | 🌟 | [`class-methods-use-this`][class-methods-use-this] | +| [`prefer-function-over-method`] | 🌟 | [`@typescript-eslint/class-methods-use-this`] | | [`prefer-method-signature`] | ✅ | [`@typescript-eslint/method-signature-style`] | | [`prefer-switch`] | 🛑 | N/A | | [`prefer-template`] | 🌟 | [`prefer-template`][prefer-template] | @@ -566,7 +566,6 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [object-shorthand]: https://eslint.org/docs/rules/object-shorthand [brace-style]: https://eslint.org/docs/rules/brace-style [one-var]: https://eslint.org/docs/rules/one-var -[class-methods-use-this]: https://eslint.org/docs/rules/class-methods-use-this [prefer-template]: https://eslint.org/docs/rules/prefer-template [quotes]: https://eslint.org/docs/rules/quotes [semi]: https://eslint.org/docs/rules/semi @@ -598,6 +597,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/await-thenable`]: https://typescript-eslint.io/rules/await-thenable [`@typescript-eslint/ban-types`]: https://typescript-eslint.io/rules/ban-types [`@typescript-eslint/ban-ts-comment`]: https://typescript-eslint.io/rules/ban-ts-comment +[`@typescript-eslint/class-methods-use-this`]: https://typescript-eslint.io/rules/class-methods-use-this [`@typescript-eslint/consistent-type-assertions`]: https://typescript-eslint.io/rules/consistent-type-assertions [`@typescript-eslint/consistent-type-definitions`]: https://typescript-eslint.io/rules/consistent-type-definitions [`@typescript-eslint/explicit-member-accessibility`]: https://typescript-eslint.io/rules/explicit-member-accessibility diff --git a/packages/eslint-plugin/docs/rules/TEMPLATE.md b/packages/eslint-plugin/docs/rules/TEMPLATE.md index ddc6def34a5c..3fb311ad2a16 100644 --- a/packages/eslint-plugin/docs/rules/TEMPLATE.md +++ b/packages/eslint-plugin/docs/rules/TEMPLATE.md @@ -1,6 +1,10 @@ +--- +description: '' +--- + > 🛑 This file is source code, not the primary documentation location! 🛑 > -> See **https://typescript-eslint.io/rules/your-rule-name** for documentation. +> See **https://typescript-eslint.io/rules/RULE_NAME_REPLACEME** for documentation. ## Examples diff --git a/packages/eslint-plugin/docs/rules/class-methods-use-this.md b/packages/eslint-plugin/docs/rules/class-methods-use-this.md new file mode 100644 index 000000000000..3195333cbbe9 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/class-methods-use-this.md @@ -0,0 +1,51 @@ +--- +description: 'Enforce that class methods utilize `this`.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/class-methods-use-this** for documentation. + +## Examples + +This rule extends the base [`eslint/class-methods-use-this`](https://eslint.org/docs/rules/class-methods-use-this) rule. +It adds support for ignoring `override` methods or methods on classes that implement an interface. + +## Options + +This rule adds the following options: + +```ts +interface Options extends BaseClassMethodsUseThisOptions { + ignoreOverrideMethods?: boolean; + ignoreClassesThatImplementAnInterface?: boolean; +} + +const defaultOptions: Options = { + ...baseClassMethodsUseThisOptions, + ignoreOverrideMethods: false, + ignoreClassesThatImplementAnInterface: false, +}; +``` + +### `ignoreOverrideMethods` + +Example of a correct code when `ignoreOverrideMethods` is set to `true`: + +```ts +class X { + override method() {} + override property = () => {}; +} +``` + +### `ignoreClassesThatImplementAnInterface` + +Example of a correct code when `ignoreClassesThatImplementAnInterface` is set to `true`: + +```ts +class X implements Y { + method() {} + property = () => {}; +} +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 184a241a66fd..d0bd265b0996 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -19,6 +19,8 @@ export = { 'brace-style': 'off', '@typescript-eslint/brace-style': 'error', '@typescript-eslint/class-literal-property-style': 'error', + 'class-methods-use-this': 'off', + '@typescript-eslint/class-methods-use-this': 'error', 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': 'error', 'comma-spacing': 'off', diff --git a/packages/eslint-plugin/src/rules/class-methods-use-this.ts b/packages/eslint-plugin/src/rules/class-methods-use-this.ts new file mode 100644 index 000000000000..562429ef4c27 --- /dev/null +++ b/packages/eslint-plugin/src/rules/class-methods-use-this.ts @@ -0,0 +1,265 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import * as util from '../util'; + +type Options = [ + { + exceptMethods?: string[]; + enforceForClassFields?: boolean; + ignoreOverrideMethods?: boolean; + ignoreClassesThatImplementAnInterface?: boolean; + }, +]; +type MessageIds = 'missingThis'; + +export default util.createRule({ + name: 'class-methods-use-this', + meta: { + type: 'suggestion', + docs: { + description: 'Enforce that class methods utilize `this`', + extendsBaseRule: true, + requiresTypeChecking: false, + }, + fixable: 'code', + hasSuggestions: false, + schema: [ + { + type: 'object', + properties: { + exceptMethods: { + type: 'array', + description: + 'Allows specified method names to be ignored with this rule', + items: { + type: 'string', + }, + }, + enforceForClassFields: { + type: 'boolean', + description: + 'Enforces that functions used as instance field initializers utilize `this`', + default: true, + }, + ignoreOverrideMethods: { + type: 'boolean', + description: 'Ingore members marked with the `override` modifier', + }, + ignoreClassesThatImplementAnInterface: { + type: 'boolean', + description: + 'Ignore classes that specifically implement some interface', + }, + }, + additionalProperties: false, + }, + ], + messages: { + missingThis: "Expected 'this' to be used by class {{name}}.", + }, + }, + defaultOptions: [ + { + enforceForClassFields: true, + exceptMethods: [], + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], + create( + context, + [ + { + enforceForClassFields, + exceptMethods: exceptMethodsRaw, + ignoreClassesThatImplementAnInterface, + ignoreOverrideMethods, + }, + ], + ) { + const exceptMethods = new Set(exceptMethodsRaw); + type Stack = + | { + member: null; + class: null; + parent: Stack | undefined; + usesThis: boolean; + } + | { + member: TSESTree.MethodDefinition | TSESTree.PropertyDefinition; + class: TSESTree.ClassDeclaration | TSESTree.ClassExpression; + parent: Stack | undefined; + usesThis: boolean; + }; + let stack: Stack | undefined; + + const sourceCode = context.getSourceCode(); + + function pushContext( + member?: TSESTree.MethodDefinition | TSESTree.PropertyDefinition, + ): void { + if (member && member.parent?.type === AST_NODE_TYPES.ClassBody) { + stack = { + member, + class: member.parent.parent as + | TSESTree.ClassDeclaration + | TSESTree.ClassExpression, + usesThis: false, + parent: stack, + }; + } else { + stack = { + member: null, + class: null, + usesThis: false, + parent: stack, + }; + } + } + + function enterFunction( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + if ( + node.parent?.type === AST_NODE_TYPES.MethodDefinition || + node.parent?.type === AST_NODE_TYPES.PropertyDefinition + ) { + pushContext(node.parent); + } else { + pushContext(); + } + } + + /** + * Pop `this` used flag from the stack. + */ + function popContext(): Stack | undefined { + const oldStack = stack; + stack = stack?.parent; + return oldStack; + } + + /** + * Check if the node is an instance method not excluded by config + */ + function isIncludedInstanceMethod( + node: NonNullable, + ): node is NonNullable { + if ( + node.static || + (node.type === AST_NODE_TYPES.MethodDefinition && + node.kind === 'constructor') || + (node.type === AST_NODE_TYPES.PropertyDefinition && + !enforceForClassFields) + ) { + return false; + } + + if (node.computed || exceptMethods.size === 0) { + return true; + } + + const hashIfNeeded = + node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : ''; + const name = + node.key.type === AST_NODE_TYPES.Literal + ? util.getStaticStringValue(node.key) + : node.key.name || ''; + + return !exceptMethods.has(hashIfNeeded + (name ?? '')); + } + + /** + * Checks if we are leaving a function that is a method, and reports if 'this' has not been used. + * Static methods and the constructor are exempt. + * Then pops the context off the stack. + */ + function exitFunction( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + const stackContext = popContext(); + if ( + stackContext?.member == null || + stackContext.class == null || + stackContext.usesThis || + (ignoreOverrideMethods && stackContext.member.override) || + (ignoreClassesThatImplementAnInterface && + stackContext.class.implements != null) + ) { + return; + } + + if (isIncludedInstanceMethod(stackContext.member)) { + context.report({ + node, + loc: util.getFunctionHeadLoc(node, sourceCode), + messageId: 'missingThis', + data: { + name: util.getFunctionNameWithKind(node), + }, + }); + } + } + + return { + // function declarations have their own `this` context + FunctionDeclaration(): void { + pushContext(); + }, + 'FunctionDeclaration:exit'(): void { + popContext(); + }, + + FunctionExpression(node): void { + enterFunction(node); + }, + 'FunctionExpression:exit'(node): void { + exitFunction(node); + }, + ...(enforceForClassFields + ? { + 'PropertyDefinition > ArrowFunctionExpression.value'( + node: TSESTree.ArrowFunctionExpression, + ): void { + enterFunction(node); + }, + 'PropertyDefinition > ArrowFunctionExpression.value:exit'( + node: TSESTree.ArrowFunctionExpression, + ): void { + exitFunction(node); + }, + } + : {}), + + /* + * Class field value are implicit functions. + */ + 'PropertyDefinition > *.key:exit'(): void { + pushContext(); + }, + 'PropertyDefinition:exit'(): void { + popContext(); + }, + + /* + * Class static blocks are implicit functions. They aren't required to use `this`, + * but we have to push context so that it captures any use of `this` in the static block + * separately from enclosing contexts, because static blocks have their own `this` and it + * shouldn't count as used `this` in enclosing contexts. + */ + StaticBlock(): void { + pushContext(); + }, + 'StaticBlock:exit'(): void { + popContext(); + }, + + 'ThisExpression, Super'(): void { + if (stack) { + stack.usesThis = true; + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index f5cf92c28d16..44aedd6198e1 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -7,6 +7,7 @@ import banTypes from './ban-types'; import blockSpacing from './block-spacing'; import braceStyle from './brace-style'; import classLiteralPropertyStyle from './class-literal-property-style'; +import classMethodsUseThis from './class-methods-use-this'; import commaDangle from './comma-dangle'; import commaSpacing from './comma-spacing'; import consistentGenericConstructors from './consistent-generic-constructors'; @@ -141,6 +142,7 @@ export default { 'block-spacing': blockSpacing, 'brace-style': braceStyle, 'class-literal-property-style': classLiteralPropertyStyle, + 'class-methods-use-this': classMethodsUseThis, 'comma-dangle': commaDangle, 'comma-spacing': commaSpacing, 'consistent-generic-constructors': consistentGenericConstructors, diff --git a/packages/eslint-plugin/src/util/getStaticStringValue.ts b/packages/eslint-plugin/src/util/getStaticStringValue.ts new file mode 100644 index 000000000000..6eeaf9af8ce3 --- /dev/null +++ b/packages/eslint-plugin/src/util/getStaticStringValue.ts @@ -0,0 +1,49 @@ +// adapted from https://github.com/eslint/eslint/blob/5bdaae205c3a0089ea338b382df59e21d5b06436/lib/rules/utils/ast-utils.js#L191-L230 + +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { isNullLiteral } from './isNullLiteral'; + +/** + * Returns the result of the string conversion applied to the evaluated value of the given expression node, + * if it can be determined statically. + * + * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. + * In all other cases, this function returns `null`. + * @param node Expression node. + * @returns String value if it can be determined. Otherwise, `null`. + */ +export function getStaticStringValue(node: TSESTree.Node): string | null { + switch (node.type) { + case AST_NODE_TYPES.Literal: + // eslint-disable-next-line eqeqeq -- intentional strict comparison for literal value + if (node.value === null) { + if (isNullLiteral(node)) { + return String(node.value); // "null" + } + if ('regex' in node) { + return `/${node.regex.pattern}/${node.regex.flags}`; + } + + if ('bigint' in node) { + return node.bigint; + } + + // Otherwise, this is an unknown literal. The function will return null. + } else { + return String(node.value); + } + break; + + case AST_NODE_TYPES.TemplateLiteral: + if (node.expressions.length === 0 && node.quasis.length === 1) { + return node.quasis[0].value.cooked; + } + break; + + // no default + } + + return null; +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 53a19a96d368..5e9994a136d9 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -5,6 +5,7 @@ export * from './collectUnusedVariables'; export * from './createRule'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; +export * from './getStaticStringValue'; export * from './getStringLength'; export * from './getThisExpression'; export * from './getWrappingFixer'; diff --git a/packages/eslint-plugin/src/util/isNullLiteral.ts b/packages/eslint-plugin/src/util/isNullLiteral.ts index 85bf45882123..d59a926c5aaa 100644 --- a/packages/eslint-plugin/src/util/isNullLiteral.ts +++ b/packages/eslint-plugin/src/util/isNullLiteral.ts @@ -1,6 +1,6 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -export function isNullLiteral(i: TSESTree.Node): boolean { +export function isNullLiteral(i: TSESTree.Node): i is TSESTree.NullLiteral { return i.type === AST_NODE_TYPES.Literal && i.value == null; } diff --git a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts new file mode 100644 index 000000000000..fd79ad54d0b2 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts @@ -0,0 +1,431 @@ +/* eslint-disable @typescript-eslint/internal/plugin-test-formatting -- +keeping eslint core formatting on purpose to make upstream diffing easier and so we don't need to edit line/cols */ +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import rule from '../../../src/rules/class-methods-use-this'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('class-methods-use-this', rule, { + valid: [ + { code: 'class A { constructor() {} }', parserOptions: { ecmaVersion: 6 } }, + { code: 'class A { foo() {this} }', parserOptions: { ecmaVersion: 6 } }, + { + code: "class A { foo() {this.bar = 'bar';} }", + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { foo() {bar(this);} }', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A extends B { foo() {super.foo();} }', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { foo() { if(true) { return this; } } }', + parserOptions: { ecmaVersion: 6 }, + }, + { code: 'class A { static foo() {} }', parserOptions: { ecmaVersion: 6 } }, + { code: '({ a(){} });', parserOptions: { ecmaVersion: 6 } }, + { + code: 'class A { foo() { () => this; } }', + parserOptions: { ecmaVersion: 6 }, + }, + { code: '({ a: function () {} });', parserOptions: { ecmaVersion: 6 } }, + { + code: 'class A { foo() {this} bar() {} }', + options: [{ exceptMethods: ['bar'] }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { "foo"() { } }', + options: [{ exceptMethods: ['foo'] }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { 42() { } }', + options: [{ exceptMethods: ['42'] }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { foo = function() {this} }', + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { foo = () => {this} }', + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { foo = () => {super.toString} }', + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { static foo = function() {} }', + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { static foo = () => {} }', + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { #bar() {} }', + options: [{ exceptMethods: ['#bar'] }], + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { foo = function () {} }', + options: [{ enforceForClassFields: false }], + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { foo = () => {} }', + options: [{ enforceForClassFields: false }], + parserOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class A { foo() { return class { [this.foo] = 1 }; } }', + parserOptions: { ecmaVersion: 2022 }, + }, + { code: 'class A { static {} }', parserOptions: { ecmaVersion: 2022 } }, + ], + invalid: [ + { + code: 'class A { foo() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {/**this**/} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {var a = function () {this};} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {var a = function () {var b = function(){this}};} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {window.this} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: "class A { foo() {that.this = 'this';} }", + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() { () => undefined; } }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {} bar() {} }', + options: [{ exceptMethods: ['bar'] }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + ], + }, + { + code: 'class A { foo() {} hasOwnProperty() {} }', + options: [{ exceptMethods: ['foo'] }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 20, + messageId: 'missingThis', + data: { name: "method 'hasOwnProperty'" }, + }, + ], + }, + { + code: 'class A { [foo]() {} }', + options: [{ exceptMethods: ['foo'] }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 11, + messageId: 'missingThis', + data: { name: 'method' }, + }, + ], + }, + { + code: 'class A { #foo() { } foo() {} #bar() {} }', + options: [{ exceptMethods: ['#foo'] }], + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 22, + messageId: 'missingThis', + data: { name: "method 'foo'" }, + }, + { + type: AST_NODE_TYPES.FunctionExpression, + line: 1, + column: 31, + messageId: 'missingThis', + data: { name: 'private method #bar' }, + }, + ], + }, + { + code: "class A { foo(){} 'bar'(){} 123(){} [`baz`](){} [a](){} [f(a)](){} get quux(){} set[a](b){} *quuux(){} }", + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 11, + }, + { + messageId: 'missingThis', + data: { name: "method 'bar'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 19, + }, + { + messageId: 'missingThis', + data: { name: "method '123'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 29, + }, + { + messageId: 'missingThis', + data: { name: "method 'baz'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 37, + }, + { + messageId: 'missingThis', + data: { name: 'method' }, + type: AST_NODE_TYPES.FunctionExpression, + column: 49, + }, + { + messageId: 'missingThis', + data: { name: 'method' }, + type: AST_NODE_TYPES.FunctionExpression, + column: 57, + }, + { + messageId: 'missingThis', + data: { name: "getter 'quux'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 68, + }, + { + messageId: 'missingThis', + data: { name: 'setter' }, + type: AST_NODE_TYPES.FunctionExpression, + column: 81, + }, + { + messageId: 'missingThis', + data: { name: "generator method 'quuux'" }, + type: AST_NODE_TYPES.FunctionExpression, + column: 93, + }, + ], + }, + { + code: 'class A { foo = function() {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + column: 11, + endColumn: 25, + }, + ], + }, + { + code: 'class A { foo = () => {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + column: 11, + endColumn: 17, + }, + ], + }, + { + code: 'class A { #foo = function() {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: 'private method #foo' }, + column: 11, + endColumn: 26, + }, + ], + }, + { + code: 'class A { #foo = () => {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: 'private method #foo' }, + column: 11, + endColumn: 18, + }, + ], + }, + { + code: 'class A { #foo() {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: 'private method #foo' }, + column: 11, + endColumn: 15, + }, + ], + }, + { + code: 'class A { get #foo() {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: 'private getter #foo' }, + column: 11, + endColumn: 19, + }, + ], + }, + { + code: 'class A { set #foo(x) {} }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: 'private setter #foo' }, + column: 11, + endColumn: 19, + }, + ], + }, + { + code: 'class A { foo () { return class { foo = this }; } }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + column: 11, + endColumn: 15, + }, + ], + }, + { + code: 'class A { foo () { return function () { foo = this }; } }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + column: 11, + endColumn: 15, + }, + ], + }, + { + code: 'class A { foo () { return class { static { this; } } } }', + parserOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'missingThis', + data: { name: "method 'foo'" }, + column: 11, + endColumn: 15, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts new file mode 100644 index 000000000000..4a422f4fc600 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts @@ -0,0 +1,185 @@ +import rule from '../../../src/rules/class-methods-use-this'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('class-methods-use-this', rule, { + valid: [ + { + code: ` +class Foo implements Bar { + method() {} +} + `, + options: [{ ignoreClassesThatImplementAnInterface: true }], + }, + { + code: ` +class Foo { + override method() {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo implements Bar { + override method() {} +} + `, + options: [ + { + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, + }, + ], + }, + { + code: ` +class Foo implements Bar { + property = () => {}; +} + `, + options: [{ ignoreClassesThatImplementAnInterface: true }], + }, + { + code: ` +class Foo { + override property = () => {}; +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo implements Bar { + override property = () => {}; +} + `, + options: [ + { + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, + }, + ], + }, + { + code: ` +class Foo implements Bar { + property = () => {}; +} + `, + options: [ + { + ignoreClassesThatImplementAnInterface: false, + enforceForClassFields: false, + }, + ], + }, + { + code: ` +class Foo { + override property = () => {}; +} + `, + options: [ + { + ignoreOverrideMethods: false, + enforceForClassFields: false, + }, + ], + }, + ], + invalid: [ + { + code: ` +class Foo implements Bar { + method() {} +} + `, + options: [{ ignoreClassesThatImplementAnInterface: false }], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + { + code: ` +class Foo { + override method() {} +} + `, + options: [{ ignoreOverrideMethods: false }], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + { + code: ` +class Foo implements Bar { + override method() {} +} + `, + options: [ + { + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + { + code: ` +class Foo implements Bar { + property = () => {}; +} + `, + options: [{ ignoreClassesThatImplementAnInterface: false }], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + { + code: ` +class Foo { + override property = () => {}; +} + `, + options: [{ ignoreOverrideMethods: false }], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + { + code: ` +class Foo implements Bar { + override property = () => {}; +} + `, + options: [ + { + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], + errors: [ + { + messageId: 'missingThis', + }, + ], + }, + ], +});