2025-04-19 23:12:19 -04:00

172 lines
4.7 KiB
JavaScript

/**
* @fileoverview Rule to flag when the same variable is declared more then once.
* @author Ilya Volodin
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require('./utils/ast-utils');
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
defaultOptions: [{ builtinGlobals: true }],
docs: {
description: 'Disallow variable redeclaration',
recommended: true,
url: 'https://eslint.org/docs/latest/rules/no-redeclare',
},
messages: {
redeclared: "'{{id}}' is already defined.",
redeclaredAsBuiltin:
"'{{id}}' is already defined as a built-in global variable.",
redeclaredBySyntax:
"'{{id}}' is already defined by a variable declaration.",
},
schema: [
{
type: 'object',
properties: {
builtinGlobals: { type: 'boolean' },
},
additionalProperties: false,
},
],
},
create(context) {
const [{ builtinGlobals }] = context.options;
const sourceCode = context.sourceCode;
/**
* Iterate declarations of a given variable.
* @param {escope.variable} variable The variable object to iterate declarations.
* @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations.
*/
function* iterateDeclarations(variable) {
if (
builtinGlobals &&
(variable.eslintImplicitGlobalSetting === 'readonly' ||
variable.eslintImplicitGlobalSetting === 'writable')
) {
yield { type: 'builtin' };
}
for (const id of variable.identifiers) {
yield { type: 'syntax', node: id, loc: id.loc };
}
if (variable.eslintExplicitGlobalComments) {
for (const comment of variable.eslintExplicitGlobalComments) {
yield {
type: 'comment',
node: comment,
loc: astUtils.getNameLocationInGlobalDirectiveComment(
sourceCode,
comment,
variable.name
),
};
}
}
}
/**
* Find variables in a given scope and flag redeclared ones.
* @param {Scope} scope An eslint-scope scope object.
* @returns {void}
* @private
*/
function findVariablesInScope(scope) {
for (const variable of scope.variables) {
const [declaration, ...extraDeclarations] =
iterateDeclarations(variable);
if (extraDeclarations.length === 0) {
continue;
}
/*
* If the type of a declaration is different from the type of
* the first declaration, it shows the location of the first
* declaration.
*/
const detailMessageId =
declaration.type === 'builtin' ?
'redeclaredAsBuiltin'
: 'redeclaredBySyntax';
const data = { id: variable.name };
// Report extra declarations.
for (const { type, node, loc } of extraDeclarations) {
const messageId =
type === declaration.type ? 'redeclared' : detailMessageId;
context.report({ node, loc, messageId, data });
}
}
}
/**
* Find variables in the current scope.
* @param {ASTNode} node The node of the current scope.
* @returns {void}
* @private
*/
function checkForBlock(node) {
const scope = sourceCode.getScope(node);
/*
* In ES5, some node type such as `BlockStatement` doesn't have that scope.
* `scope.block` is a different node in such a case.
*/
if (scope.block === node) {
findVariablesInScope(scope);
}
}
return {
Program(node) {
const scope = sourceCode.getScope(node);
findVariablesInScope(scope);
// Node.js or ES modules has a special scope.
if (
scope.type === 'global' &&
scope.childScopes[0] &&
// The special scope's block is the Program node.
scope.block === scope.childScopes[0].block
) {
findVariablesInScope(scope.childScopes[0]);
}
},
FunctionDeclaration: checkForBlock,
FunctionExpression: checkForBlock,
ArrowFunctionExpression: checkForBlock,
StaticBlock: checkForBlock,
BlockStatement: checkForBlock,
ForStatement: checkForBlock,
ForInStatement: checkForBlock,
ForOfStatement: checkForBlock,
SwitchStatement: checkForBlock,
};
},
};