]>
Commit | Line | Data |
---|---|---|
eb39fafa | 1 | /** |
8f9d1d4d | 2 | * @fileoverview A rule to disallow `this` keywords in contexts where the value of `this` is `undefined`. |
eb39fafa DC |
3 | * @author Toru Nagashima |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
8f9d1d4d DC |
14 | //------------------------------------------------------------------------------ |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Determines if the given code path is a code path with lexical `this` binding. | |
20 | * That is, if `this` within the code path refers to `this` of surrounding code path. | |
21 | * @param {CodePath} codePath Code path. | |
22 | * @param {ASTNode} node Node that started the code path. | |
23 | * @returns {boolean} `true` if it is a code path with lexical `this` binding. | |
24 | */ | |
25 | function isCodePathWithLexicalThis(codePath, node) { | |
26 | return codePath.origin === "function" && node.type === "ArrowFunctionExpression"; | |
27 | } | |
28 | ||
eb39fafa DC |
29 | //------------------------------------------------------------------------------ |
30 | // Rule Definition | |
31 | //------------------------------------------------------------------------------ | |
32 | ||
34eeec05 | 33 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
34 | module.exports = { |
35 | meta: { | |
36 | type: "suggestion", | |
37 | ||
38 | docs: { | |
8f9d1d4d | 39 | description: "Disallow use of `this` in contexts where the value of `this` is `undefined`", |
eb39fafa DC |
40 | recommended: false, |
41 | url: "https://eslint.org/docs/rules/no-invalid-this" | |
42 | }, | |
43 | ||
44 | schema: [ | |
45 | { | |
46 | type: "object", | |
47 | properties: { | |
48 | capIsConstructor: { | |
49 | type: "boolean", | |
50 | default: true | |
51 | } | |
52 | }, | |
53 | additionalProperties: false | |
54 | } | |
55 | ], | |
56 | ||
57 | messages: { | |
58 | unexpectedThis: "Unexpected 'this'." | |
59 | } | |
60 | }, | |
61 | ||
62 | create(context) { | |
63 | const options = context.options[0] || {}; | |
64 | const capIsConstructor = options.capIsConstructor !== false; | |
65 | const stack = [], | |
66 | sourceCode = context.getSourceCode(); | |
67 | ||
68 | /** | |
69 | * Gets the current checking context. | |
70 | * | |
71 | * The return value has a flag that whether or not `this` keyword is valid. | |
72 | * The flag is initialized when got at the first time. | |
73 | * @returns {{valid: boolean}} | |
74 | * an object which has a flag that whether or not `this` keyword is valid. | |
75 | */ | |
76 | stack.getCurrent = function() { | |
77 | const current = this[this.length - 1]; | |
78 | ||
79 | if (!current.init) { | |
80 | current.init = true; | |
81 | current.valid = !astUtils.isDefaultThisBinding( | |
82 | current.node, | |
83 | sourceCode, | |
84 | { capIsConstructor } | |
85 | ); | |
86 | } | |
87 | return current; | |
88 | }; | |
89 | ||
8f9d1d4d | 90 | return { |
eb39fafa | 91 | |
8f9d1d4d DC |
92 | onCodePathStart(codePath, node) { |
93 | if (isCodePathWithLexicalThis(codePath, node)) { | |
94 | return; | |
95 | } | |
eb39fafa | 96 | |
8f9d1d4d DC |
97 | if (codePath.origin === "program") { |
98 | const scope = context.getScope(); | |
99 | const features = context.parserOptions.ecmaFeatures || {}; | |
100 | ||
101 | // `this` at the top level of scripts always refers to the global object | |
102 | stack.push({ | |
103 | init: true, | |
104 | node, | |
105 | valid: !( | |
106 | node.sourceType === "module" || | |
107 | (features.globalReturn && scope.childScopes[0].isStrict) | |
108 | ) | |
109 | }); | |
eb39fafa | 110 | |
8f9d1d4d DC |
111 | return; |
112 | } | |
eb39fafa | 113 | |
8f9d1d4d DC |
114 | /* |
115 | * `init: false` means that `valid` isn't determined yet. | |
116 | * Most functions don't use `this`, and the calculation for `valid` | |
117 | * is relatively costly, so we'll calculate it lazily when the first | |
118 | * `this` within the function is traversed. A special case are non-strict | |
119 | * functions, because `this` refers to the global object and therefore is | |
120 | * always valid, so we can set `init: true` right away. | |
121 | */ | |
eb39fafa | 122 | stack.push({ |
8f9d1d4d | 123 | init: !context.getScope().isStrict, |
eb39fafa | 124 | node, |
8f9d1d4d | 125 | valid: true |
eb39fafa DC |
126 | }); |
127 | }, | |
128 | ||
8f9d1d4d DC |
129 | onCodePathEnd(codePath, node) { |
130 | if (isCodePathWithLexicalThis(codePath, node)) { | |
131 | return; | |
132 | } | |
133 | ||
eb39fafa DC |
134 | stack.pop(); |
135 | }, | |
136 | ||
eb39fafa DC |
137 | // Reports if `this` of the current context is invalid. |
138 | ThisExpression(node) { | |
139 | const current = stack.getCurrent(); | |
140 | ||
141 | if (current && !current.valid) { | |
142 | context.report({ | |
143 | node, | |
144 | messageId: "unexpectedThis" | |
145 | }); | |
146 | } | |
147 | } | |
148 | }; | |
149 | } | |
150 | }; |