Skip to content

Commit 18ea3b1

Browse files
authored
feat(eslint-plugin): [class-methods-use-this] add extension rule (typescript-eslint#6457)
1 parent f813147 commit 18ea3b1

12 files changed

+1053
-4
lines changed

packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th
185185
| [`one-line`] | 🌟 | [`brace-style`][brace-style] or [Prettier] |
186186
| [`one-variable-per-declaration`] | 🌟 | [`one-var`][one-var] |
187187
| [`ordered-imports`] | 🌓 | [`import/order`] |
188-
| [`prefer-function-over-method`] | 🌟 | [`class-methods-use-this`][class-methods-use-this] |
188+
| [`prefer-function-over-method`] | 🌟 | [`@typescript-eslint/class-methods-use-this`] |
189189
| [`prefer-method-signature`] || [`@typescript-eslint/method-signature-style`] |
190190
| [`prefer-switch`] | 🛑 | N/A |
191191
| [`prefer-template`] | 🌟 | [`prefer-template`][prefer-template] |
@@ -566,7 +566,6 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
566566
[object-shorthand]: https://eslint.org/docs/rules/object-shorthand
567567
[brace-style]: https://eslint.org/docs/rules/brace-style
568568
[one-var]: https://eslint.org/docs/rules/one-var
569-
[class-methods-use-this]: https://eslint.org/docs/rules/class-methods-use-this
570569
[prefer-template]: https://eslint.org/docs/rules/prefer-template
571570
[quotes]: https://eslint.org/docs/rules/quotes
572571
[semi]: https://eslint.org/docs/rules/semi
@@ -598,6 +597,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
598597
[`@typescript-eslint/await-thenable`]: https://typescript-eslint.io/rules/await-thenable
599598
[`@typescript-eslint/ban-types`]: https://typescript-eslint.io/rules/ban-types
600599
[`@typescript-eslint/ban-ts-comment`]: https://typescript-eslint.io/rules/ban-ts-comment
600+
[`@typescript-eslint/class-methods-use-this`]: https://typescript-eslint.io/rules/class-methods-use-this
601601
[`@typescript-eslint/consistent-type-assertions`]: https://typescript-eslint.io/rules/consistent-type-assertions
602602
[`@typescript-eslint/consistent-type-definitions`]: https://typescript-eslint.io/rules/consistent-type-definitions
603603
[`@typescript-eslint/explicit-member-accessibility`]: https://typescript-eslint.io/rules/explicit-member-accessibility

packages/eslint-plugin/docs/rules/TEMPLATE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
---
2+
description: '<Description from rule metadata here>'
3+
---
4+
15
> 🛑 This file is source code, not the primary documentation location! 🛑
26
>
3-
> See **https://typescript-eslint.io/rules/your-rule-name** for documentation.
7+
> See **https://typescript-eslint.io/rules/RULE_NAME_REPLACEME** for documentation.
48
59
## Examples
610

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
description: 'Enforce that class methods utilize `this`.'
3+
---
4+
5+
> 🛑 This file is source code, not the primary documentation location! 🛑
6+
>
7+
> See **https://typescript-eslint.io/rules/class-methods-use-this** for documentation.
8+
9+
## Examples
10+
11+
This rule extends the base [`eslint/class-methods-use-this`](https://eslint.org/docs/rules/class-methods-use-this) rule.
12+
It adds support for ignoring `override` methods or methods on classes that implement an interface.
13+
14+
## Options
15+
16+
This rule adds the following options:
17+
18+
```ts
19+
interface Options extends BaseClassMethodsUseThisOptions {
20+
ignoreOverrideMethods?: boolean;
21+
ignoreClassesThatImplementAnInterface?: boolean;
22+
}
23+
24+
const defaultOptions: Options = {
25+
...baseClassMethodsUseThisOptions,
26+
ignoreOverrideMethods: false,
27+
ignoreClassesThatImplementAnInterface: false,
28+
};
29+
```
30+
31+
### `ignoreOverrideMethods`
32+
33+
Makes the rule to ignores any class member explicitly marked with `override`.
34+
35+
Example of a correct code when `ignoreOverrideMethods` is set to `true`:
36+
37+
```ts
38+
class X {
39+
override method() {}
40+
override property = () => {};
41+
}
42+
```
43+
44+
### `ignoreClassesThatImplementAnInterface`
45+
46+
Makes the rule ignore all class members that are defined within a class that `implements` a type.
47+
48+
It's important to note that this option does not only apply to members defined in the interface as that would require type information.
49+
50+
Example of a correct code when `ignoreClassesThatImplementAnInterface` is set to `true`:
51+
52+
```ts
53+
class X implements Y {
54+
method() {}
55+
property = () => {};
56+
}
57+
```

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export = {
1919
'brace-style': 'off',
2020
'@typescript-eslint/brace-style': 'error',
2121
'@typescript-eslint/class-literal-property-style': 'error',
22+
'class-methods-use-this': 'off',
23+
'@typescript-eslint/class-methods-use-this': 'error',
2224
'comma-dangle': 'off',
2325
'@typescript-eslint/comma-dangle': 'error',
2426
'comma-spacing': 'off',
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3+
4+
import * as util from '../util';
5+
6+
type Options = [
7+
{
8+
exceptMethods?: string[];
9+
enforceForClassFields?: boolean;
10+
ignoreOverrideMethods?: boolean;
11+
ignoreClassesThatImplementAnInterface?: boolean;
12+
},
13+
];
14+
type MessageIds = 'missingThis';
15+
16+
export default util.createRule<Options, MessageIds>({
17+
name: 'class-methods-use-this',
18+
meta: {
19+
type: 'suggestion',
20+
docs: {
21+
description: 'Enforce that class methods utilize `this`',
22+
extendsBaseRule: true,
23+
requiresTypeChecking: false,
24+
},
25+
fixable: 'code',
26+
hasSuggestions: false,
27+
schema: [
28+
{
29+
type: 'object',
30+
properties: {
31+
exceptMethods: {
32+
type: 'array',
33+
description:
34+
'Allows specified method names to be ignored with this rule',
35+
items: {
36+
type: 'string',
37+
},
38+
},
39+
enforceForClassFields: {
40+
type: 'boolean',
41+
description:
42+
'Enforces that functions used as instance field initializers utilize `this`',
43+
default: true,
44+
},
45+
ignoreOverrideMethods: {
46+
type: 'boolean',
47+
description: 'Ingore members marked with the `override` modifier',
48+
},
49+
ignoreClassesThatImplementAnInterface: {
50+
type: 'boolean',
51+
description:
52+
'Ignore classes that specifically implement some interface',
53+
},
54+
},
55+
additionalProperties: false,
56+
},
57+
],
58+
messages: {
59+
missingThis: "Expected 'this' to be used by class {{name}}.",
60+
},
61+
},
62+
defaultOptions: [
63+
{
64+
enforceForClassFields: true,
65+
exceptMethods: [],
66+
ignoreClassesThatImplementAnInterface: false,
67+
ignoreOverrideMethods: false,
68+
},
69+
],
70+
create(
71+
context,
72+
[
73+
{
74+
enforceForClassFields,
75+
exceptMethods: exceptMethodsRaw,
76+
ignoreClassesThatImplementAnInterface,
77+
ignoreOverrideMethods,
78+
},
79+
],
80+
) {
81+
const exceptMethods = new Set(exceptMethodsRaw);
82+
type Stack =
83+
| {
84+
member: null;
85+
class: null;
86+
parent: Stack | undefined;
87+
usesThis: boolean;
88+
}
89+
| {
90+
member: TSESTree.MethodDefinition | TSESTree.PropertyDefinition;
91+
class: TSESTree.ClassDeclaration | TSESTree.ClassExpression;
92+
parent: Stack | undefined;
93+
usesThis: boolean;
94+
};
95+
let stack: Stack | undefined;
96+
97+
const sourceCode = context.getSourceCode();
98+
99+
function pushContext(
100+
member?: TSESTree.MethodDefinition | TSESTree.PropertyDefinition,
101+
): void {
102+
if (member?.parent.type === AST_NODE_TYPES.ClassBody) {
103+
stack = {
104+
member,
105+
class: member.parent.parent as
106+
| TSESTree.ClassDeclaration
107+
| TSESTree.ClassExpression,
108+
usesThis: false,
109+
parent: stack,
110+
};
111+
} else {
112+
stack = {
113+
member: null,
114+
class: null,
115+
usesThis: false,
116+
parent: stack,
117+
};
118+
}
119+
}
120+
121+
function enterFunction(
122+
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
123+
): void {
124+
if (
125+
node.parent.type === AST_NODE_TYPES.MethodDefinition ||
126+
node.parent.type === AST_NODE_TYPES.PropertyDefinition
127+
) {
128+
pushContext(node.parent);
129+
} else {
130+
pushContext();
131+
}
132+
}
133+
134+
/**
135+
* Pop `this` used flag from the stack.
136+
*/
137+
function popContext(): Stack | undefined {
138+
const oldStack = stack;
139+
stack = stack?.parent;
140+
return oldStack;
141+
}
142+
143+
/**
144+
* Check if the node is an instance method not excluded by config
145+
*/
146+
function isIncludedInstanceMethod(
147+
node: NonNullable<Stack['member']>,
148+
): node is NonNullable<Stack['member']> {
149+
if (
150+
node.static ||
151+
(node.type === AST_NODE_TYPES.MethodDefinition &&
152+
node.kind === 'constructor') ||
153+
(node.type === AST_NODE_TYPES.PropertyDefinition &&
154+
!enforceForClassFields)
155+
) {
156+
return false;
157+
}
158+
159+
if (node.computed || exceptMethods.size === 0) {
160+
return true;
161+
}
162+
163+
const hashIfNeeded =
164+
node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
165+
const name =
166+
node.key.type === AST_NODE_TYPES.Literal
167+
? util.getStaticStringValue(node.key)
168+
: node.key.name || '';
169+
170+
return !exceptMethods.has(hashIfNeeded + (name ?? ''));
171+
}
172+
173+
/**
174+
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
175+
* Static methods and the constructor are exempt.
176+
* Then pops the context off the stack.
177+
*/
178+
function exitFunction(
179+
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
180+
): void {
181+
const stackContext = popContext();
182+
if (
183+
stackContext?.member == null ||
184+
stackContext.class == null ||
185+
stackContext.usesThis ||
186+
(ignoreOverrideMethods && stackContext.member.override) ||
187+
(ignoreClassesThatImplementAnInterface &&
188+
stackContext.class.implements != null)
189+
) {
190+
return;
191+
}
192+
193+
if (isIncludedInstanceMethod(stackContext.member)) {
194+
context.report({
195+
node,
196+
loc: util.getFunctionHeadLoc(node, sourceCode),
197+
messageId: 'missingThis',
198+
data: {
199+
name: util.getFunctionNameWithKind(node),
200+
},
201+
});
202+
}
203+
}
204+
205+
return {
206+
// function declarations have their own `this` context
207+
FunctionDeclaration(): void {
208+
pushContext();
209+
},
210+
'FunctionDeclaration:exit'(): void {
211+
popContext();
212+
},
213+
214+
FunctionExpression(node): void {
215+
enterFunction(node);
216+
},
217+
'FunctionExpression:exit'(node): void {
218+
exitFunction(node);
219+
},
220+
...(enforceForClassFields
221+
? {
222+
'PropertyDefinition > ArrowFunctionExpression.value'(
223+
node: TSESTree.ArrowFunctionExpression,
224+
): void {
225+
enterFunction(node);
226+
},
227+
'PropertyDefinition > ArrowFunctionExpression.value:exit'(
228+
node: TSESTree.ArrowFunctionExpression,
229+
): void {
230+
exitFunction(node);
231+
},
232+
}
233+
: {}),
234+
235+
/*
236+
* Class field value are implicit functions.
237+
*/
238+
'PropertyDefinition > *.key:exit'(): void {
239+
pushContext();
240+
},
241+
'PropertyDefinition:exit'(): void {
242+
popContext();
243+
},
244+
245+
/*
246+
* Class static blocks are implicit functions. They aren't required to use `this`,
247+
* but we have to push context so that it captures any use of `this` in the static block
248+
* separately from enclosing contexts, because static blocks have their own `this` and it
249+
* shouldn't count as used `this` in enclosing contexts.
250+
*/
251+
StaticBlock(): void {
252+
pushContext();
253+
},
254+
'StaticBlock:exit'(): void {
255+
popContext();
256+
},
257+
258+
'ThisExpression, Super'(): void {
259+
if (stack) {
260+
stack.usesThis = true;
261+
}
262+
},
263+
};
264+
},
265+
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import banTypes from './ban-types';
77
import blockSpacing from './block-spacing';
88
import braceStyle from './brace-style';
99
import classLiteralPropertyStyle from './class-literal-property-style';
10+
import classMethodsUseThis from './class-methods-use-this';
1011
import commaDangle from './comma-dangle';
1112
import commaSpacing from './comma-spacing';
1213
import consistentGenericConstructors from './consistent-generic-constructors';
@@ -141,6 +142,7 @@ export default {
141142
'block-spacing': blockSpacing,
142143
'brace-style': braceStyle,
143144
'class-literal-property-style': classLiteralPropertyStyle,
145+
'class-methods-use-this': classMethodsUseThis,
144146
'comma-dangle': commaDangle,
145147
'comma-spacing': commaSpacing,
146148
'consistent-generic-constructors': consistentGenericConstructors,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy