Skip to content

Commit

Permalink
feat(eslint-plugin): [no-shadow] ignoreOnInitialization option (#4603)
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-Cena committed Mar 31, 2022
1 parent 8c98d16 commit 068ea9b
Show file tree
Hide file tree
Showing 3 changed files with 549 additions and 0 deletions.
138 changes: 138 additions & 0 deletions packages/eslint-plugin/src/rules/no-shadow.ts
Expand Up @@ -18,6 +18,7 @@ type Options = [
allow?: string[];
builtinGlobals?: boolean;
hoist?: 'all' | 'functions' | 'never';
ignoreOnInitialization?: boolean;
ignoreTypeValueShadow?: boolean;
ignoreFunctionTypeParameterNameValueShadow?: boolean;
},
Expand Down Expand Up @@ -49,6 +50,9 @@ export default util.createRule<Options, MessageIds>({
type: 'string',
},
},
ignoreOnInitialization: {
type: 'boolean',
},
ignoreTypeValueShadow: {
type: 'boolean',
},
Expand All @@ -68,6 +72,7 @@ export default util.createRule<Options, MessageIds>({
allow: [],
builtinGlobals: false,
hoist: 'functions',
ignoreOnInitialization: false,
ignoreTypeValueShadow: true,
ignoreFunctionTypeParameterNameValueShadow: true,
},
Expand Down Expand Up @@ -314,6 +319,135 @@ 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 (
[
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.ClassDeclaration,
AST_NODE_TYPES.FunctionExpression,
AST_NODE_TYPES.ClassExpression,
AST_NODE_TYPES.ArrowFunctionExpression,
AST_NODE_TYPES.CatchClause,
AST_NODE_TYPES.ImportDeclaration,
AST_NODE_TYPES.ExportNamedDeclaration,
].includes(node.type)
) {
break;
}

node = node.parent;
}

return false;
}

/**
* Checks if a variable is inside the initializer of scopeVar.
*
Expand Down Expand Up @@ -455,6 +589,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

0 comments on commit 068ea9b

Please sign in to comment.