blob: beb742d93a793824b88ef23f3299c06adaff680b [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
2 * @fileoverview Rule to enforce that all class methods use 'this'.
3 * @author Patrick Williams
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +01009// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
Yang Guo4fd355c2019-09-19 10:59:03 +020015// Rule Definition
16//------------------------------------------------------------------------------
17
18module.exports = {
19 meta: {
20 type: "suggestion",
21
22 docs: {
23 description: "enforce that class methods utilize `this`",
Yang Guo4fd355c2019-09-19 10:59:03 +020024 recommended: false,
25 url: "https://eslint.org/docs/rules/class-methods-use-this"
26 },
27
28 schema: [{
29 type: "object",
30 properties: {
31 exceptMethods: {
32 type: "array",
33 items: {
34 type: "string"
35 }
Tim van der Lippe0fb47802021-11-08 16:23:10 +000036 },
37 enforceForClassFields: {
38 type: "boolean",
39 default: true
Yang Guo4fd355c2019-09-19 10:59:03 +020040 }
41 },
42 additionalProperties: false
43 }],
44
45 messages: {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010046 missingThis: "Expected 'this' to be used by class {{name}}."
Yang Guo4fd355c2019-09-19 10:59:03 +020047 }
48 },
49 create(context) {
50 const config = Object.assign({}, context.options[0]);
Tim van der Lippe0fb47802021-11-08 16:23:10 +000051 const enforceForClassFields = config.enforceForClassFields !== false;
Yang Guo4fd355c2019-09-19 10:59:03 +020052 const exceptMethods = new Set(config.exceptMethods || []);
53
54 const stack = [];
55
56 /**
Tim van der Lippe0fb47802021-11-08 16:23:10 +000057 * Push `this` used flag initialized with `false` onto the stack.
58 * @returns {void}
59 */
60 function pushContext() {
61 stack.push(false);
62 }
63
64 /**
65 * Pop `this` used flag from the stack.
66 * @returns {boolean | undefined} `this` used flag
67 */
68 function popContext() {
69 return stack.pop();
70 }
71
72 /**
Yang Guo4fd355c2019-09-19 10:59:03 +020073 * Initializes the current context to false and pushes it onto the stack.
74 * These booleans represent whether 'this' has been used in the context.
75 * @returns {void}
76 * @private
77 */
78 function enterFunction() {
Tim van der Lippe0fb47802021-11-08 16:23:10 +000079 pushContext();
Yang Guo4fd355c2019-09-19 10:59:03 +020080 }
81
82 /**
83 * Check if the node is an instance method
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010084 * @param {ASTNode} node node to check
Yang Guo4fd355c2019-09-19 10:59:03 +020085 * @returns {boolean} True if its an instance method
86 * @private
87 */
88 function isInstanceMethod(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +000089 switch (node.type) {
90 case "MethodDefinition":
91 return !node.static && node.kind !== "constructor";
92 case "PropertyDefinition":
93 return !node.static && enforceForClassFields;
94 default:
95 return false;
96 }
Yang Guo4fd355c2019-09-19 10:59:03 +020097 }
98
99 /**
100 * Check if the node is an instance method not excluded by config
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100101 * @param {ASTNode} node node to check
Yang Guo4fd355c2019-09-19 10:59:03 +0200102 * @returns {boolean} True if it is an instance method, and not excluded by config
103 * @private
104 */
105 function isIncludedInstanceMethod(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000106 if (isInstanceMethod(node)) {
107 if (node.computed) {
108 return true;
109 }
110
111 const hashIfNeeded = node.key.type === "PrivateIdentifier" ? "#" : "";
112 const name = node.key.type === "Literal"
113 ? astUtils.getStaticStringValue(node.key)
114 : (node.key.name || "");
115
116 return !exceptMethods.has(hashIfNeeded + name);
117 }
118 return false;
Yang Guo4fd355c2019-09-19 10:59:03 +0200119 }
120
121 /**
122 * Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
123 * Static methods and the constructor are exempt.
124 * Then pops the context off the stack.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100125 * @param {ASTNode} node A function node that was entered.
Yang Guo4fd355c2019-09-19 10:59:03 +0200126 * @returns {void}
127 * @private
128 */
129 function exitFunction(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000130 const methodUsesThis = popContext();
Yang Guo4fd355c2019-09-19 10:59:03 +0200131
132 if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
133 context.report({
134 node,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000135 loc: astUtils.getFunctionHeadLoc(node, context.getSourceCode()),
Yang Guo4fd355c2019-09-19 10:59:03 +0200136 messageId: "missingThis",
137 data: {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100138 name: astUtils.getFunctionNameWithKind(node)
Yang Guo4fd355c2019-09-19 10:59:03 +0200139 }
140 });
141 }
142 }
143
144 /**
145 * Mark the current context as having used 'this'.
146 * @returns {void}
147 * @private
148 */
149 function markThisUsed() {
150 if (stack.length) {
151 stack[stack.length - 1] = true;
152 }
153 }
154
155 return {
156 FunctionDeclaration: enterFunction,
157 "FunctionDeclaration:exit": exitFunction,
158 FunctionExpression: enterFunction,
159 "FunctionExpression:exit": exitFunction,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000160
161 /*
162 * Class field value are implicit functions.
163 */
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000164 "PropertyDefinition > *.key:exit": pushContext,
Tim van der Lippe0124c682021-11-23 15:12:10 +0000165 "PropertyDefinition:exit": popContext,
166
167 /*
168 * Class static blocks are implicit functions. They aren't required to use `this`,
169 * but we have to push context so that it captures any use of `this` in the static block
170 * separately from enclosing contexts, because static blocks have their own `this` and it
171 * shouldn't count as used `this` in enclosing contexts.
172 */
173 StaticBlock: pushContext,
174 "StaticBlock:exit": popContext,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000175
Yang Guo4fd355c2019-09-19 10:59:03 +0200176 ThisExpression: markThisUsed,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000177 Super: markThisUsed,
178 ...(
179 enforceForClassFields && {
180 "PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
181 "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction
182 }
183 )
Yang Guo4fd355c2019-09-19 10:59:03 +0200184 };
185 }
186};