Skip to content

Commit a383657

Browse files
authored
consistent-function-scoping: Fix TypeScript false positives for lexical this (#2885)
1 parent 73b043b commit a383657

4 files changed

Lines changed: 107 additions & 8 deletions

File tree

rules/consistent-function-scoping.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
2-
import {getReferences, isNodeMatches} from './utils/index.js';
2+
import {getReferences, isNodeContainsLexicalThis, isNodeMatches} from './utils/index.js';
33
import {functionTypes} from './ast/index.js';
44

55
const MESSAGE_ID = 'consistent-function-scoping';
@@ -94,10 +94,11 @@ const isReactHook = scope =>
9494
scope.block?.parent?.callee
9595
&& isNodeMatches(scope.block.parent.callee, reactHooks);
9696

97-
const isArrowFunctionWithThis = scope =>
98-
scope.type === 'function'
99-
&& scope.block?.type === 'ArrowFunctionExpression'
100-
&& (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
97+
const isArrowFunctionNodeWithThis = (node, visitorKeys) =>
98+
node.type === 'ArrowFunctionExpression'
99+
// We avoid `scope.thisFound` because parser scope metadata differs; AST lexical checks are consistent.
100+
// Include both params and body, because parameter defaults can reference lexical `this`.
101+
&& isNodeContainsLexicalThis(node, visitorKeys);
101102

102103
const iifeFunctionTypes = new Set([
103104
'FunctionExpression',
@@ -145,10 +146,13 @@ function handleNestedArrowFunctions(parentNode, node) {
145146
return parentNode;
146147
}
147148

148-
function checkNode(node, scopeManager) {
149+
function checkNode(node, scopeManager, sourceCode) {
149150
const scope = scopeManager.acquire(node);
150151

151-
if (!scope || isArrowFunctionWithThis(scope)) {
152+
if (
153+
!scope
154+
|| isArrowFunctionNodeWithThis(node, sourceCode.visitorKeys)
155+
) {
152156
return true;
153157
}
154158

@@ -220,7 +224,7 @@ const create = context => {
220224
return;
221225
}
222226

223-
if (checkNode(node, scopeManager)) {
227+
if (checkNode(node, scopeManager, sourceCode)) {
224228
return;
225229
}
226230

rules/utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export {default as isNewExpressionWithParentheses} from './is-new-expression-wit
4242
export {default as isNumber} from './is-number.js';
4343
export {default as isNodeValueNotDomNode} from './is-node-value-not-dom-node.js';
4444
export {default as isNodeValueNotFunction} from './is-node-value-not-function.js';
45+
export {default as isNodeContainsLexicalThis} from './is-node-contains-lexical-this.js';
4546
export {default as isOnSameLine} from './is-on-same-line.js';
4647
export {default as isSameIdentifier} from './is-same-identifier.js';
4748
export {default as isSameReference} from './is-same-reference.js';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
Check whether a node subtree contains lexical `this`.
3+
4+
This is parser-agnostic and intentionally does not use `scope.thisFound`, because that flag is inconsistent across supported parsers.
5+
*/
6+
const isNodeContainsLexicalThis = (node, visitorKeys) => {
7+
if (node.type === 'ThisExpression') {
8+
return true;
9+
}
10+
11+
if (
12+
node.type === 'FunctionDeclaration'
13+
|| node.type === 'FunctionExpression'
14+
) {
15+
// `this` inside non-arrow functions is rebound and does not affect outer arrows.
16+
return false;
17+
}
18+
19+
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
20+
// Class bodies create their own `this`, but computed keys/superclass are evaluated in outer scope.
21+
if (node.superClass && isNodeContainsLexicalThis(node.superClass, visitorKeys)) {
22+
return true;
23+
}
24+
25+
for (const classElement of node.body.body) {
26+
if (classElement.computed && isNodeContainsLexicalThis(classElement.key, visitorKeys)) {
27+
return true;
28+
}
29+
}
30+
31+
return false;
32+
}
33+
34+
const keys = visitorKeys[node.type];
35+
36+
if (!keys) {
37+
return false;
38+
}
39+
40+
for (const key of keys) {
41+
const value = node[key];
42+
43+
if (!value) {
44+
continue;
45+
}
46+
47+
if (Array.isArray(value)) {
48+
for (const childNode of value) {
49+
if (childNode && isNodeContainsLexicalThis(childNode, visitorKeys)) {
50+
return true;
51+
}
52+
}
53+
54+
continue;
55+
}
56+
57+
if (isNodeContainsLexicalThis(value, visitorKeys)) {
58+
return true;
59+
}
60+
}
61+
62+
return false;
63+
};
64+
65+
export default isNodeContainsLexicalThis;

test/consistent-function-scoping.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ test({
248248
return doBar();
249249
};
250250
`,
251+
outdent`
252+
function doFoo(Foo) {
253+
const doBar = (value = this) => value;
254+
return doBar();
255+
};
256+
`,
257+
outdent`
258+
function doFoo(Foo) {
259+
const doBar = () => class C {
260+
[this.x]() {}
261+
};
262+
return doBar();
263+
};
264+
`,
251265
// `arguments`
252266
outdent`
253267
function doFoo(Foo) {
@@ -953,6 +967,21 @@ test.typescript({
953967
return b(1)(2);
954968
}
955969
`,
970+
// #2088
971+
outdent`
972+
class Foo {
973+
public bar = new FinalizationRegistry(() => {
974+
console.log(this);
975+
})
976+
}
977+
`,
978+
// #2088
979+
outdent`
980+
function foo(this: unknown) {
981+
const bar = () => console.log(this);
982+
return [].map(() => bar);
983+
}
984+
`,
956985
],
957986
invalid: [],
958987
});

0 commit comments

Comments
 (0)