diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index ef87ebc29d7..75f5ffd8515 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -24,6 +24,9 @@ type Options = [ }, ]; +const SENTINEL_TYPE = + /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u; + export default util.createRule({ name: 'no-shadow', meta: { @@ -319,6 +322,124 @@ export default util.createRule({ ); } + /** + * Checks whether or not a given location is inside of the range of a given node. + * @param node An node to check. + * @param location A location to check. + * @returns `true` if the location is inside of the range of the node. + */ + function isInRange( + node: TSESTree.Node | null, + location: number, + ): boolean | null { + return node && node.range[0] <= location && location <= node.range[1]; + } + + /** + * Searches from the current node through its ancestry to find a matching node. + * @param node a node to get. + * @param match a callback that checks whether or not the node verifies its condition or not. + * @returns the matching node. + */ + function findSelfOrAncestor( + node: TSESTree.Node | undefined, + match: (node: TSESTree.Node) => boolean, + ): TSESTree.Node | undefined { + let currentNode = node; + + while (currentNode && !match(currentNode)) { + currentNode = currentNode.parent; + } + return currentNode; + } + + /** + * Finds function's outer scope. + * @param scope Function's own scope. + * @returns Function's outer scope. + */ + function getOuterScope( + scope: TSESLint.Scope.Scope, + ): TSESLint.Scope.Scope | null { + const upper = scope.upper; + + if (upper?.type === 'function-expression-name') { + return upper.upper; + } + return upper; + } + + /** + * Checks if a variable and a shadowedVariable have the same init pattern ancestor. + * @param variable a variable to check. + * @param shadowedVariable a shadowedVariable to check. + * @returns Whether or not the variable and the shadowedVariable have the same init pattern ancestor. + */ + function isInitPatternNode( + variable: TSESLint.Scope.Variable, + shadowedVariable: TSESLint.Scope.Variable, + ): boolean { + const outerDef = shadowedVariable.defs[0]; + + if (!outerDef) { + return false; + } + + const { variableScope } = variable.scope; + + if ( + !( + (variableScope.block.type === + AST_NODE_TYPES.ArrowFunctionExpression || + variableScope.block.type === AST_NODE_TYPES.FunctionExpression) && + getOuterScope(variableScope) === shadowedVariable.scope + ) + ) { + return false; + } + + const fun = variableScope.block; + const { parent } = fun; + + const callExpression = findSelfOrAncestor( + parent, + node => node.type === AST_NODE_TYPES.CallExpression, + ); + + if (!callExpression) { + return false; + } + + let node: TSESTree.Node | undefined = outerDef.name; + const location = callExpression.range[1]; + + while (node) { + if (node.type === AST_NODE_TYPES.VariableDeclarator) { + if (isInRange(node.init, location)) { + return true; + } + if ( + (node.parent?.parent?.type === AST_NODE_TYPES.ForInStatement || + node.parent?.parent?.type === AST_NODE_TYPES.ForOfStatement) && + isInRange(node.parent.parent.right, location) + ) { + return true; + } + break; + } else if (node.type === AST_NODE_TYPES.AssignmentPattern) { + if (isInRange(node.right, location)) { + return true; + } + } else if (SENTINEL_TYPE.test(node.type)) { + break; + } + + node = node.parent; + } + + return false; + } + /** * Checks if a variable is inside the initializer of scopeVar. * @@ -460,6 +581,10 @@ export default util.createRule({ (shadowed.identifiers.length > 0 || (options.builtinGlobals && isESLintGlobal)) && !isOnInitializer(variable, shadowed) && + !( + options.ignoreOnInitialization && + isInitPatternNode(variable, shadowed) + ) && !(options.hoist !== 'all' && isInTdz(variable, shadowed)) ) { context.report({ diff --git a/packages/eslint-plugin/tests/rules/no-shadow.test.ts b/packages/eslint-plugin/tests/rules/no-shadow.test.ts index 635dff307e1..a89c12f5c89 100644 --- a/packages/eslint-plugin/tests/rules/no-shadow.test.ts +++ b/packages/eslint-plugin/tests/rules/no-shadow.test.ts @@ -202,6 +202,156 @@ function doThing(foo: number) {} `, options: [{ ignoreTypeValueShadow: true }], }, + { + code: 'const a = [].find(a => a);', + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +const a = [].find(function (a) { + return a; +}); + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const [a = [].find(a => true)] = dummy;', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const { a = [].find(a => true) } = dummy;', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'function func(a = [].find(a => true)) {}', + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +for (const a in [].find(a => true)) { +} + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +for (const a of [].find(a => true)) { +} + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: "const a = [].map(a => true).filter(a => a === 'b');", + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +const a = [] + .map(a => true) + .filter(a => a === 'b') + .find(a => a === 'c'); + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const { a } = (({ a }) => ({ a }))();', + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +const person = people.find(item => { + const person = item.name; + return person === 'foo'; +}); + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var y = bar || foo(y => y);', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var y = bar && foo(y => y);', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var z = bar(foo(z => z));', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var z = boo(bar(foo(z => z)));', + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +var match = function (person) { + return person.name === 'foo'; +}; +const person = [].find(match); + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const a = foo(x || (a => {}));', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const { a = 1 } = foo(a => {});', + options: [{ ignoreOnInitialization: true }], + }, + { + code: "const person = { ...people.find(person => person.firstName.startsWith('s')) };", + options: [{ ignoreOnInitialization: true }], + parserOptions: { ecmaVersion: 2021 }, + }, + { + code: ` +const person = { + firstName: people + .filter(person => person.firstName.startsWith('s')) + .map(person => person.firstName)[0], +}; + `, + options: [{ ignoreOnInitialization: true }], + parserOptions: { ecmaVersion: 2021 }, + }, + { + code: ` +() => { + const y = foo(y => y); +}; + `, + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const x = (x => x)();', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var y = bar || (y => y)();', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var y = bar && (y => y)();', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'var x = (x => x)((y => y)());', + options: [{ ignoreOnInitialization: true }], + }, + { + code: 'const { a = 1 } = (a => {})();', + options: [{ ignoreOnInitialization: true }], + }, + { + code: ` +() => { + const y = (y => y)(); +}; + `, + options: [{ ignoreOnInitialization: true }], + }, + { code: 'const [x = y => y] = [].map(y => y);' }, ], invalid: [ { @@ -1612,5 +1762,321 @@ declare module 'bar' { }, ], }, + { + code: ` +let x = foo((x, y) => {}); +let y; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ hoist: 'all' }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 5, + }, + type: AST_NODE_TYPES.Identifier, + }, + { + messageId: 'noShadow', + data: { + name: 'y', + shadowedLine: 2, + shadowedColumn: 5, + }, + type: AST_NODE_TYPES.Identifier, + }, + ], + }, + { + code: ` +const a = fn(() => { + class C { + fn() { + const a = 42; + return a; + } + } + return new C(); +}); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'a', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 44, + }, + ], + }, + { + code: 'function a() {}\nfoo(a => {});', + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'a', + shadowedLine: 1, + shadowedColumn: 10, + }, + type: AST_NODE_TYPES.Identifier, + line: 2, + column: 5, + }, + ], + }, + { + code: ` +const a = fn(() => { + function C() { + this.fn = function () { + const a = 42; + return a; + }; + } + return new C(); +}); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'a', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 62, + }, + ], + }, + { + code: ` +const x = foo(() => { + const bar = () => { + return x => {}; + }; + return bar; +}); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 50, + }, + ], + }, + { + code: ` +const x = foo(() => { + return { bar(x) {} }; +}); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 36, + }, + ], + }, + { + code: ` +const x = () => { + foo(x => x); +}; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 23, + }, + ], + }, + { + code: ` +const foo = () => { + let x; + bar(x => x); +}; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 25, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 32, + }, + ], + }, + { + code: ` +foo(() => { + const x = x => x; +}); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 19, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 23, + }, + ], + }, + { + code: ` +const foo = x => { + bar(x => {}); +}; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 14, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 26, + }, + ], + }, + { + code: ` +let x = ((x, y) => {})(); +let y; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ hoist: 'all' }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 5, + }, + type: AST_NODE_TYPES.Identifier, + }, + { + messageId: 'noShadow', + data: { + name: 'y', + shadowedLine: 2, + shadowedColumn: 5, + }, + type: AST_NODE_TYPES.Identifier, + }, + ], + }, + { + code: ` +const a = (() => { + class C { + fn() { + const a = 42; + return a; + } + } + return new C(); +})(); + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'a', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 42, + }, + ], + }, + { + code: ` +const x = () => { + (x => x)(); +}; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ ignoreOnInitialization: true }], + errors: [ + { + messageId: 'noShadow', + data: { + name: 'x', + shadowedLine: 1, + shadowedColumn: 7, + }, + type: AST_NODE_TYPES.Identifier, + line: 1, + column: 20, + }, + ], + }, ], });