Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-shadow] ignoreOnInitialization option #4603

Merged
merged 7 commits into from Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 125 additions & 0 deletions packages/eslint-plugin/src/rules/no-shadow.ts
Expand Up @@ -24,6 +24,9 @@ type Options = [
},
];

const SENTINEL_TYPE =
/^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved

export default util.createRule<Options, MessageIds>({
name: 'no-shadow',
meta: {
Expand Down Expand Up @@ -319,6 +322,124 @@ export default util.createRule<Options, MessageIds>({
);
}

/**
* 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.
*
Expand Down Expand Up @@ -460,6 +581,10 @@ export default util.createRule<Options, MessageIds>({
(shadowed.identifiers.length > 0 ||
(options.builtinGlobals && isESLintGlobal)) &&
!isOnInitializer(variable, shadowed) &&
!(
options.ignoreOnInitialization &&
isInitPatternNode(variable, shadowed)
) &&
!(options.hoist !== 'all' && isInTdz(variable, shadowed))
) {
context.report({
Expand Down