blob: 1af6084ed8be783c4b8a638fcafb670ac02cab0f [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
Tim van der Lippe0ceb4652022-01-06 14:23:36 +010018/** @type {import('../shared/types').Rule} */
Yang Guo4fd355c2019-09-19 10:59:03 +020019module.exports = {
20 meta: {
21 type: "suggestion",
22
23 docs: {
24 description: "enforce that class methods utilize `this`",
Yang Guo4fd355c2019-09-19 10:59:03 +020025 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 }
Tim van der Lippe0fb47802021-11-08 16:23:10 +000037 },
38 enforceForClassFields: {
39 type: "boolean",
40 default: true
Yang Guo4fd355c2019-09-19 10:59:03 +020041 }
42 },
43 additionalProperties: false
44 }],
45
46 messages: {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010047 missingThis: "Expected 'this' to be used by class {{name}}."
Yang Guo4fd355c2019-09-19 10:59:03 +020048 }
49 },
50 create(context) {
51 const config = Object.assign({}, context.options[0]);
Tim van der Lippe0fb47802021-11-08 16:23:10 +000052 const enforceForClassFields = config.enforceForClassFields !== false;
Yang Guo4fd355c2019-09-19 10:59:03 +020053 const exceptMethods = new Set(config.exceptMethods || []);
54
55 const stack = [];
56
57 /**
Tim van der Lippe0fb47802021-11-08 16:23:10 +000058 * 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
73 /**
Yang Guo4fd355c2019-09-19 10:59:03 +020074 * 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() {
Tim van der Lippe0fb47802021-11-08 16:23:10 +000080 pushContext();
Yang Guo4fd355c2019-09-19 10:59:03 +020081 }
82
83 /**
84 * Check if the node is an instance method
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010085 * @param {ASTNode} node node to check
Yang Guo4fd355c2019-09-19 10:59:03 +020086 * @returns {boolean} True if its an instance method
87 * @private
88 */
89 function isInstanceMethod(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +000090 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 }
Yang Guo4fd355c2019-09-19 10:59:03 +020098 }
99
100 /**
101 * Check if the node is an instance method not excluded by config
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100102 * @param {ASTNode} node node to check
Yang Guo4fd355c2019-09-19 10:59:03 +0200103 * @returns {boolean} True if it is an instance method, and not excluded by config
104 * @private
105 */
106 function isIncludedInstanceMethod(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000107 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;
Yang Guo4fd355c2019-09-19 10:59:03 +0200120 }
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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100126 * @param {ASTNode} node A function node that was entered.
Yang Guo4fd355c2019-09-19 10:59:03 +0200127 * @returns {void}
128 * @private
129 */
130 function exitFunction(node) {
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000131 const methodUsesThis = popContext();
Yang Guo4fd355c2019-09-19 10:59:03 +0200132
133 if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
134 context.report({
135 node,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000136 loc: astUtils.getFunctionHeadLoc(node, context.getSourceCode()),
Yang Guo4fd355c2019-09-19 10:59:03 +0200137 messageId: "missingThis",
138 data: {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100139 name: astUtils.getFunctionNameWithKind(node)
Yang Guo4fd355c2019-09-19 10:59:03 +0200140 }
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,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000161
162 /*
163 * Class field value are implicit functions.
164 */
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000165 "PropertyDefinition > *.key:exit": pushContext,
Tim van der Lippe0124c682021-11-23 15:12:10 +0000166 "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,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000176
Yang Guo4fd355c2019-09-19 10:59:03 +0200177 ThisExpression: markThisUsed,
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000178 Super: markThisUsed,
179 ...(
180 enforceForClassFields && {
181 "PropertyDefinition > ArrowFunctionExpression.value": enterFunction,
182 "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction
183 }
184 )
Yang Guo4fd355c2019-09-19 10:59:03 +0200185 };
186 }
187};