UNPKG

11.6 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22const utils_1 = require("@typescript-eslint/utils");
23const tsutils = __importStar(require("tsutils"));
24const ts = __importStar(require("typescript"));
25const util = __importStar(require("../util"));
26/**
27 * The following is a list of exceptions to the rule
28 * Generated via the following script.
29 * This is statically defined to save making purposely invalid calls every lint run
30 * ```
31SUPPORTED_GLOBALS.flatMap(namespace => {
32 const object = window[namespace];
33 return Object.getOwnPropertyNames(object)
34 .filter(
35 name =>
36 !name.startsWith('_') &&
37 typeof object[name] === 'function',
38 )
39 .map(name => {
40 try {
41 const x = object[name];
42 x();
43 } catch (e) {
44 if (e.message.includes("called on non-object")) {
45 return `${namespace}.${name}`;
46 }
47 }
48 });
49}).filter(Boolean);
50 * ```
51 */
52const nativelyNotBoundMembers = new Set([
53 'Promise.all',
54 'Promise.race',
55 'Promise.resolve',
56 'Promise.reject',
57 'Promise.allSettled',
58 'Object.defineProperties',
59 'Object.defineProperty',
60 'Reflect.defineProperty',
61 'Reflect.deleteProperty',
62 'Reflect.get',
63 'Reflect.getOwnPropertyDescriptor',
64 'Reflect.getPrototypeOf',
65 'Reflect.has',
66 'Reflect.isExtensible',
67 'Reflect.ownKeys',
68 'Reflect.preventExtensions',
69 'Reflect.set',
70 'Reflect.setPrototypeOf',
71]);
72const SUPPORTED_GLOBALS = [
73 'Number',
74 'Object',
75 'String',
76 'RegExp',
77 'Symbol',
78 'Array',
79 'Proxy',
80 'Date',
81 'Infinity',
82 'Atomics',
83 'Reflect',
84 'console',
85 'Math',
86 'JSON',
87 'Intl',
88];
89const nativelyBoundMembers = SUPPORTED_GLOBALS.map(namespace => {
90 if (!(namespace in global)) {
91 // node.js might not have namespaces like Intl depending on compilation options
92 // https://nodejs.org/api/intl.html#intl_options_for_building_node_js
93 return [];
94 }
95 const object = global[namespace];
96 return Object.getOwnPropertyNames(object)
97 .filter(name => !name.startsWith('_') &&
98 typeof object[name] === 'function')
99 .map(name => `${namespace}.${name}`);
100})
101 .reduce((arr, names) => arr.concat(names), [])
102 .filter(name => !nativelyNotBoundMembers.has(name));
103const isNotImported = (symbol, currentSourceFile) => {
104 const { valueDeclaration } = symbol;
105 if (!valueDeclaration) {
106 // working around https://github.com/microsoft/TypeScript/issues/31294
107 return false;
108 }
109 return (!!currentSourceFile &&
110 currentSourceFile !== valueDeclaration.getSourceFile());
111};
112const getNodeName = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier ? node.name : null;
113const getMemberFullName = (node) => `${getNodeName(node.object)}.${getNodeName(node.property)}`;
114const BASE_MESSAGE = 'Avoid referencing unbound methods which may cause unintentional scoping of `this`.';
115exports.default = util.createRule({
116 name: 'unbound-method',
117 meta: {
118 docs: {
119 description: 'Enforces unbound methods are called with their expected scope',
120 recommended: 'error',
121 requiresTypeChecking: true,
122 },
123 messages: {
124 unbound: BASE_MESSAGE,
125 unboundWithoutThisAnnotation: BASE_MESSAGE +
126 '\n' +
127 'If your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.',
128 },
129 schema: [
130 {
131 type: 'object',
132 properties: {
133 ignoreStatic: {
134 type: 'boolean',
135 },
136 },
137 additionalProperties: false,
138 },
139 ],
140 type: 'problem',
141 },
142 defaultOptions: [
143 {
144 ignoreStatic: false,
145 },
146 ],
147 create(context, [{ ignoreStatic }]) {
148 const parserServices = util.getParserServices(context);
149 const checker = parserServices.program.getTypeChecker();
150 const currentSourceFile = parserServices.program.getSourceFile(context.getFilename());
151 function checkMethodAndReport(node, symbol) {
152 if (!symbol) {
153 return;
154 }
155 const { dangerous, firstParamIsThis } = checkMethod(symbol, ignoreStatic);
156 if (dangerous) {
157 context.report({
158 messageId: firstParamIsThis === false
159 ? 'unboundWithoutThisAnnotation'
160 : 'unbound',
161 node,
162 });
163 }
164 }
165 return {
166 MemberExpression(node) {
167 if (isSafeUse(node)) {
168 return;
169 }
170 const objectSymbol = checker.getSymbolAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node.object));
171 if (objectSymbol &&
172 nativelyBoundMembers.includes(getMemberFullName(node)) &&
173 isNotImported(objectSymbol, currentSourceFile)) {
174 return;
175 }
176 const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
177 checkMethodAndReport(node, checker.getSymbolAtLocation(originalNode));
178 },
179 'VariableDeclarator, AssignmentExpression'(node) {
180 const [idNode, initNode] = node.type === utils_1.AST_NODE_TYPES.VariableDeclarator
181 ? [node.id, node.init]
182 : [node.left, node.right];
183 if (initNode && idNode.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
184 const tsNode = parserServices.esTreeNodeToTSNodeMap.get(initNode);
185 const rightSymbol = checker.getSymbolAtLocation(tsNode);
186 const initTypes = checker.getTypeAtLocation(tsNode);
187 const notImported = rightSymbol && isNotImported(rightSymbol, currentSourceFile);
188 idNode.properties.forEach(property => {
189 if (property.type === utils_1.AST_NODE_TYPES.Property &&
190 property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
191 if (notImported &&
192 util.isIdentifier(initNode) &&
193 nativelyBoundMembers.includes(`${initNode.name}.${property.key.name}`)) {
194 return;
195 }
196 checkMethodAndReport(node, initTypes.getProperty(property.key.name));
197 }
198 });
199 }
200 },
201 };
202 },
203});
204function checkMethod(symbol, ignoreStatic) {
205 var _a, _b;
206 const { valueDeclaration } = symbol;
207 if (!valueDeclaration) {
208 // working around https://github.com/microsoft/TypeScript/issues/31294
209 return { dangerous: false };
210 }
211 switch (valueDeclaration.kind) {
212 case ts.SyntaxKind.PropertyDeclaration:
213 return {
214 dangerous: ((_a = valueDeclaration.initializer) === null || _a === void 0 ? void 0 : _a.kind) ===
215 ts.SyntaxKind.FunctionExpression,
216 };
217 case ts.SyntaxKind.MethodDeclaration:
218 case ts.SyntaxKind.MethodSignature: {
219 const decl = valueDeclaration;
220 const firstParam = decl.parameters[0];
221 const firstParamIsThis = (firstParam === null || firstParam === void 0 ? void 0 : firstParam.name.kind) === ts.SyntaxKind.Identifier &&
222 (firstParam === null || firstParam === void 0 ? void 0 : firstParam.name.escapedText) === 'this';
223 const thisArgIsVoid = firstParamIsThis &&
224 ((_b = firstParam === null || firstParam === void 0 ? void 0 : firstParam.type) === null || _b === void 0 ? void 0 : _b.kind) === ts.SyntaxKind.VoidKeyword;
225 return {
226 dangerous: !thisArgIsVoid &&
227 !(ignoreStatic &&
228 tsutils.hasModifier(valueDeclaration.modifiers, ts.SyntaxKind.StaticKeyword)),
229 firstParamIsThis,
230 };
231 }
232 }
233 return { dangerous: false };
234}
235function isSafeUse(node) {
236 const parent = node.parent;
237 switch (parent === null || parent === void 0 ? void 0 : parent.type) {
238 case utils_1.AST_NODE_TYPES.IfStatement:
239 case utils_1.AST_NODE_TYPES.ForStatement:
240 case utils_1.AST_NODE_TYPES.MemberExpression:
241 case utils_1.AST_NODE_TYPES.SwitchStatement:
242 case utils_1.AST_NODE_TYPES.UpdateExpression:
243 case utils_1.AST_NODE_TYPES.WhileStatement:
244 return true;
245 case utils_1.AST_NODE_TYPES.CallExpression:
246 return parent.callee === node;
247 case utils_1.AST_NODE_TYPES.ConditionalExpression:
248 return parent.test === node;
249 case utils_1.AST_NODE_TYPES.TaggedTemplateExpression:
250 return parent.tag === node;
251 case utils_1.AST_NODE_TYPES.UnaryExpression:
252 // the first case is safe for obvious
253 // reasons. The second one is also fine
254 // since we're returning something falsy
255 return ['typeof', '!', 'void', 'delete'].includes(parent.operator);
256 case utils_1.AST_NODE_TYPES.BinaryExpression:
257 return ['instanceof', '==', '!=', '===', '!=='].includes(parent.operator);
258 case utils_1.AST_NODE_TYPES.AssignmentExpression:
259 return (parent.operator === '=' &&
260 (node === parent.left ||
261 (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
262 node.object.type === utils_1.AST_NODE_TYPES.Super &&
263 parent.left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
264 parent.left.object.type === utils_1.AST_NODE_TYPES.ThisExpression)));
265 case utils_1.AST_NODE_TYPES.ChainExpression:
266 case utils_1.AST_NODE_TYPES.TSNonNullExpression:
267 case utils_1.AST_NODE_TYPES.TSAsExpression:
268 case utils_1.AST_NODE_TYPES.TSTypeAssertion:
269 return isSafeUse(parent);
270 case utils_1.AST_NODE_TYPES.LogicalExpression:
271 if (parent.operator === '&&' && parent.left === node) {
272 // this is safe, as && will return the left if and only if it's falsy
273 return true;
274 }
275 // in all other cases, it's likely the logical expression will return the method ref
276 // so make sure the parent is a safe usage
277 return isSafeUse(parent);
278 }
279 return false;
280}
281//# sourceMappingURL=unbound-method.js.map
\No newline at end of file