]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to enforce that all class methods use 'this'. | |
3 | * @author Patrick Williams | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Rule Definition | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
34eeec05 | 18 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
19 | module.exports = { |
20 | meta: { | |
21 | type: "suggestion", | |
22 | ||
23 | docs: { | |
24 | description: "enforce that class methods utilize `this`", | |
eb39fafa DC |
25 | recommended: false, |
26 | url: "https://eslint.org/docs/rules/class-methods-use-this" | |
27 | }, | |
28 | ||
29 | schema: [{ | |
30 | type: "object", | |
31 | properties: { | |
32 | exceptMethods: { | |
33 | type: "array", | |
34 | items: { | |
35 | type: "string" | |
36 | } | |
609c276f TL |
37 | }, |
38 | enforceForClassFields: { | |
39 | type: "boolean", | |
40 | default: true | |
eb39fafa DC |
41 | } |
42 | }, | |
43 | additionalProperties: false | |
44 | }], | |
45 | ||
46 | messages: { | |
47 | missingThis: "Expected 'this' to be used by class {{name}}." | |
48 | } | |
49 | }, | |
50 | create(context) { | |
51 | const config = Object.assign({}, context.options[0]); | |
609c276f | 52 | const enforceForClassFields = config.enforceForClassFields !== false; |
eb39fafa DC |
53 | const exceptMethods = new Set(config.exceptMethods || []); |
54 | ||
55 | const stack = []; | |
56 | ||
609c276f TL |
57 | /** |
58 | * Push `this` used flag initialized with `false` onto the stack. | |
59 | * @returns {void} | |
60 | */ | |
61 | function pushContext() { | |
62 | stack.push(false); | |
63 | } | |
64 | ||
65 | /** | |
66 | * Pop `this` used flag from the stack. | |
67 | * @returns {boolean | undefined} `this` used flag | |
68 | */ | |
69 | function popContext() { | |
70 | return stack.pop(); | |
71 | } | |
72 | ||
eb39fafa DC |
73 | /** |
74 | * Initializes the current context to false and pushes it onto the stack. | |
75 | * These booleans represent whether 'this' has been used in the context. | |
76 | * @returns {void} | |
77 | * @private | |
78 | */ | |
79 | function enterFunction() { | |
609c276f | 80 | pushContext(); |
eb39fafa DC |
81 | } |
82 | ||
83 | /** | |
84 | * Check if the node is an instance method | |
85 | * @param {ASTNode} node node to check | |
86 | * @returns {boolean} True if its an instance method | |
87 | * @private | |
88 | */ | |
89 | function isInstanceMethod(node) { | |
609c276f TL |
90 | switch (node.type) { |
91 | case "MethodDefinition": | |
92 | return !node.static && node.kind !== "constructor"; | |
93 | case "PropertyDefinition": | |
94 | return !node.static && enforceForClassFields; | |
95 | default: | |
96 | return false; | |
97 | } | |
eb39fafa DC |
98 | } |
99 | ||
100 | /** | |
101 | * Check if the node is an instance method not excluded by config | |
102 | * @param {ASTNode} node node to check | |
103 | * @returns {boolean} True if it is an instance method, and not excluded by config | |
104 | * @private | |
105 | */ | |
106 | function isIncludedInstanceMethod(node) { | |
609c276f TL |
107 | if (isInstanceMethod(node)) { |
108 | if (node.computed) { | |
109 | return true; | |
110 | } | |
111 | ||
112 | const hashIfNeeded = node.key.type === "PrivateIdentifier" ? "#" : ""; | |
113 | const name = node.key.type === "Literal" | |
114 | ? astUtils.getStaticStringValue(node.key) | |
115 | : (node.key.name || ""); | |
116 | ||
117 | return !exceptMethods.has(hashIfNeeded + name); | |
118 | } | |
119 | return false; | |
eb39fafa DC |
120 | } |
121 | ||
122 | /** | |
123 | * Checks if we are leaving a function that is a method, and reports if 'this' has not been used. | |
124 | * Static methods and the constructor are exempt. | |
125 | * Then pops the context off the stack. | |
126 | * @param {ASTNode} node A function node that was entered. | |
127 | * @returns {void} | |
128 | * @private | |
129 | */ | |
130 | function exitFunction(node) { | |
609c276f | 131 | const methodUsesThis = popContext(); |
eb39fafa DC |
132 | |
133 | if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) { | |
134 | context.report({ | |
135 | node, | |
609c276f | 136 | loc: astUtils.getFunctionHeadLoc(node, context.getSourceCode()), |
eb39fafa DC |
137 | messageId: "missingThis", |
138 | data: { | |
139 | name: astUtils.getFunctionNameWithKind(node) | |
140 | } | |
141 | }); | |
142 | } | |
143 | } | |
144 | ||
145 | /** | |
146 | * Mark the current context as having used 'this'. | |
147 | * @returns {void} | |
148 | * @private | |
149 | */ | |
150 | function markThisUsed() { | |
151 | if (stack.length) { | |
152 | stack[stack.length - 1] = true; | |
153 | } | |
154 | } | |
155 | ||
156 | return { | |
157 | FunctionDeclaration: enterFunction, | |
158 | "FunctionDeclaration:exit": exitFunction, | |
159 | FunctionExpression: enterFunction, | |
160 | "FunctionExpression:exit": exitFunction, | |
609c276f TL |
161 | |
162 | /* | |
163 | * Class field value are implicit functions. | |
164 | */ | |
165 | "PropertyDefinition > *.key:exit": pushContext, | |
166 | "PropertyDefinition:exit": popContext, | |
167 | ||
168 | /* | |
169 | * Class static blocks are implicit functions. They aren't required to use `this`, | |
170 | * but we have to push context so that it captures any use of `this` in the static block | |
171 | * separately from enclosing contexts, because static blocks have their own `this` and it | |
172 | * shouldn't count as used `this` in enclosing contexts. | |
173 | */ | |
174 | StaticBlock: pushContext, | |
175 | "StaticBlock:exit": popContext, | |
176 | ||
eb39fafa | 177 | ThisExpression: markThisUsed, |
609c276f TL |
178 | Super: markThisUsed, |
179 | ...( | |
180 | enforceForClassFields && { | |
181 | "PropertyDefinition > ArrowFunctionExpression.value": enterFunction, | |
182 | "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction | |
183 | } | |
184 | ) | |
eb39fafa DC |
185 | }; |
186 | } | |
187 | }; |