Skip to content

Commit 43677de

Browse files
authored
feat: fix handling of function and class expression names in no-shadow (#20432)
* feat: fix handling of function and class expression names in `no-shadow` Fixes #20425 * add more tests * add more tests * rename variables, update jsdoc * update docs * update docs examples * remove extra empty line * refactor
1 parent 3e5d38c commit 43677de

3 files changed

Lines changed: 585 additions & 24 deletions

File tree

docs/src/rules/no-shadow.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ d(a);
4747
if (true) {
4848
const a = 5;
4949
}
50+
51+
const f = wrap(function f() {});
52+
53+
const C = wrap(class C {});
54+
```
55+
56+
:::
57+
58+
Examples of **correct** code for this rule:
59+
60+
::: correct
61+
62+
```js
63+
/*eslint no-shadow: "error"*/
64+
65+
const a = 3;
66+
function b(c) {
67+
const d = 10;
68+
if (c) {
69+
const e = 20;
70+
}
71+
}
72+
73+
/*
74+
* Function and class names are allowed to shadow a variable
75+
* if the function/class expression is assigned to the variable
76+
* as its initializer or default value.
77+
*/
78+
const f = function f() {};
79+
const g = foo ? (bar || function g() {}) : baz;
80+
const { h = function h() {} } = obj;
81+
function qux(i = function i() {}) {}
82+
const C = class C {};
5083
```
5184

5285
:::

lib/rules/no-shadow.js

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -346,32 +346,97 @@ module.exports = {
346346
}
347347

348348
/**
349-
* Checks if a variable is inside the initializer of scopeVar.
349+
* Finds the uppermost expression node that can evaluate to the given one.
350350
*
351-
* To avoid reporting at declarations such as `var a = function a() {};`.
352-
* But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`.
353-
* @param {Object} variable The variable to check.
354-
* @param {Object} scopeVar The scope variable to look for.
355-
* @returns {boolean} Whether or not the variable is inside initializer of scopeVar.
351+
* Examples:
352+
* If given `a` in `a || foo`, it returns the `a || foo` node.
353+
* If given `a` in `foo ? a : bar`, it returns the `foo ? a : bar` node.
354+
* If given `a` in `foo ? bar : (baz && a)`, it returns the `foo ? bar : (baz && a)` node.
355+
* If given `a` in `a ? foo : bar`, it returns the `a` node.
356+
* If given `a` in `foo(a)`, it returns the `a` node.
357+
* @param {ASTNode} expression The expression node to unwrap.
358+
* @returns {ASTNode} The uppermost ancestor that can evaluate to the given node
359+
* or the given node if there is no such ancestor.
356360
*/
357-
function isOnInitializer(variable, scopeVar) {
358-
const outerScope = scopeVar.scope;
359-
const outerDef = scopeVar.defs[0];
360-
const outer = outerDef && outerDef.parent && outerDef.parent.range;
361-
const innerScope = variable.scope;
362-
const innerDef = variable.defs[0];
363-
const inner = innerDef && innerDef.name.range;
361+
function unwrapExpression(expression) {
362+
const { parent } = expression;
364363

365-
return (
366-
outer &&
367-
inner &&
368-
outer[0] < inner[0] &&
369-
inner[1] < outer[1] &&
370-
((innerDef.type === "FunctionName" &&
371-
innerDef.node.type === "FunctionExpression") ||
372-
innerDef.node.type === "ClassExpression") &&
373-
outerScope === innerScope.upper
374-
);
364+
const shouldUnwrap =
365+
parent.type === "LogicalExpression" ||
366+
(parent.type === "ConditionalExpression" &&
367+
parent.test !== expression);
368+
369+
return shouldUnwrap ? unwrapExpression(parent) : expression;
370+
}
371+
372+
/**
373+
* Checks if inner variable is the name of a function or class
374+
* that is assigned to outer variable as its initializer.
375+
*
376+
* To avoid reporting at declarations such as:
377+
* var a = function a() {};
378+
* var A = class A {};
379+
* var a = foo || function a() {};
380+
* var a = foo ? function a() {} : bar;
381+
* var { a = function a() {} } = foo;
382+
*
383+
* But it should report at declarations such as:
384+
* var a = function(a) {};
385+
* var a = function() { function a() {} };
386+
* var a = wrap(function a() {});
387+
* @param {Object} innerVariable The inner variable to check.
388+
* @param {Object} outerVariable The outer variable.
389+
* @returns {boolean} Whether or not inner variable is the name of a
390+
* function or class that is assigned to outer variable as its initializer.
391+
*/
392+
function isFunctionNameInitializerException(
393+
innerVariable,
394+
outerVariable,
395+
) {
396+
const outerDef = outerVariable.defs[0];
397+
const innerDef = innerVariable.defs[0];
398+
399+
if (!outerDef || !innerDef) {
400+
return false;
401+
}
402+
403+
if (
404+
!(
405+
(innerDef.type === "FunctionName" &&
406+
innerDef.node.type === "FunctionExpression") ||
407+
(innerDef.type === "ClassName" &&
408+
innerDef.node.type === "ClassExpression")
409+
)
410+
) {
411+
return false;
412+
}
413+
414+
const outerIdentifier = outerDef.name;
415+
let initializerNode;
416+
417+
if (outerIdentifier.parent.type === "VariableDeclarator") {
418+
initializerNode = outerIdentifier.parent.init;
419+
} else if (outerIdentifier.parent.type === "AssignmentPattern") {
420+
initializerNode = outerIdentifier.parent.right;
421+
}
422+
423+
if (!initializerNode) {
424+
return false;
425+
}
426+
427+
const nodeToCheck = innerDef.node; // FunctionExpression or ClassExpression node
428+
429+
// Exit early if the node to check isn't inside the initializer
430+
if (
431+
!(
432+
initializerNode.range[0] <= nodeToCheck.range[0] &&
433+
nodeToCheck.range[1] <= initializerNode.range[1]
434+
)
435+
) {
436+
return false;
437+
}
438+
439+
return initializerNode === unwrapExpression(nodeToCheck);
375440
}
376441

377442
/**
@@ -577,7 +642,7 @@ module.exports = {
577642
shadowed &&
578643
(shadowed.identifiers.length > 0 ||
579644
(builtinGlobals && "writeable" in shadowed)) &&
580-
!isOnInitializer(variable, shadowed) &&
645+
!isFunctionNameInitializerException(variable, shadowed) &&
581646
!(
582647
ignoreOnInitialization &&
583648
isInitPatternNode(variable, shadowed)

0 commit comments

Comments
 (0)