@mindfiredigital/eslint-plugin-hub
Version:
eslint-plugin-hub is a powerful, flexible ESLint plugin that provides a curated set of rules to enhance code readability, maintainability, and prevent common errors. Whether you're working with vanilla JavaScript, TypeScript, React, or Angular, eslint-plu
278 lines (243 loc) • 9.8 kB
JavaScript
module.exports = {
rules: {
'limit-data-scope': {
meta: {
type: 'suggestion',
docs: {
description:
'Enforces several best practices for data scoping: disallows global object modification, suggests moving variables to their narrowest functional scope, and discourages `var` usage.',
category: 'Best Practices',
recommended: false,
},
schema: [],
messages: {
noModifyGlobal:
'Avoid modifying the global object "{{objectName}}". "{{propertyName}}" should not be added globally.',
moveToNarrowerScope:
"Variable '{{variableName}}' is declared in {{declarationScopeType}} scope but appears to be used only within the '{{usageScopeIdentifier}}' {{usageScopeType}} scope. Consider moving its declaration into the '{{usageScopeIdentifier}}' scope.",
useLetConst:
"Prefer 'let' or 'const' over 'var' for variable '{{variableName}}'.",
},
},
create(context) {
const sourceCode = context.getSourceCode();
const functionInfoForNarrowestScope = new Map();
const globalObjects = new Set(['global', 'globalThis', 'window']);
function getFunctionName(node) {
if (node.id && node.id.name) {
return node.id.name;
}
if (node.parent) {
if (
node.parent.type === 'VariableDeclarator' &&
node.parent.id &&
node.parent.id.name
) {
return node.parent.id.name;
}
if (node.parent.type === 'Property' && node.parent.key) {
return (
node.parent.key.name ||
node.parent.key.value ||
'[anonymous_function]'
);
}
if (
node.parent.type === 'AssignmentExpression' &&
node.parent.left &&
node.parent.left.name
) {
return node.parent.left.name;
}
}
return '[anonymous_function]';
}
function getAllScopes(programScope) {
const scopes = [];
function collectScopes(scope) {
scopes.push(scope);
scope.childScopes.forEach(collectScopes);
}
collectScopes(programScope);
return scopes;
}
function findContainingFunctionScope(startScope, targetScope) {
let currentScope = startScope;
while (currentScope && currentScope !== targetScope) {
if (currentScope.type === 'function') {
return currentScope;
}
currentScope = currentScope.upper;
}
return null;
}
return {
':function': function (node) {
const name = getFunctionName(node);
functionInfoForNarrowestScope.set(node, name);
},
AssignmentExpression(node) {
const { left } = node;
if (left.type !== 'MemberExpression') return;
// Check if this is a direct assignment to a global object or nested assignment
function findGlobalObjectInChain(memberExpr) {
let current = memberExpr;
while (current.type === 'MemberExpression') {
if (
current.object.type === 'Identifier' &&
globalObjects.has(current.object.name)
) {
return {
objectNode: current.object,
rootMemberExpr: current,
topLevelProperty: current.property,
};
}
current = current.object;
}
return null;
}
const globalInfo = findGlobalObjectInChain(left);
if (!globalInfo) return;
const { objectNode, topLevelProperty } = globalInfo;
const scope = sourceCode.getScope(objectNode);
const reference = scope.references.find(
ref => ref.identifier === objectNode
);
let isShadowed = false;
if (
reference &&
reference.resolved &&
reference.resolved.defs.length > 0
) {
isShadowed = true;
}
if (!isShadowed) {
let propertyName = '';
if (globalInfo.rootMemberExpr.computed) {
if (topLevelProperty.type === 'Literal') {
propertyName = String(topLevelProperty.value);
} else if (topLevelProperty.type === 'Identifier') {
propertyName = topLevelProperty.name;
} else {
propertyName = '[complex]';
}
} else if (topLevelProperty.type === 'Identifier') {
propertyName = topLevelProperty.name;
}
context.report({
node: left,
messageId: 'noModifyGlobal',
data: { objectName: objectNode.name, propertyName },
});
}
},
VariableDeclaration(node) {
if (node.kind === 'var') {
let firstVariableName = '[unnamed_variable]';
if (
node.declarations.length > 0 &&
node.declarations[0].id &&
node.declarations[0].id.name
) {
firstVariableName = node.declarations[0].id.name;
}
context.report({
node: node,
messageId: 'useLetConst',
data: { variableName: firstVariableName },
});
}
},
'Program:exit': function (programNode) {
const programScope = sourceCode.getScope(programNode);
const allScopes = getAllScopes(programScope);
// Look for variables in module/global scopes
const targetScopes = allScopes.filter(
scope => scope.type === 'module' || scope.type === 'global'
);
targetScopes.forEach(scope => {
scope.variables.forEach(variable => {
// Skip if no definitions or references
if (
variable.defs.length === 0 ||
variable.references.length === 0
) {
return;
}
// Skip built-in variables and imports
if (
variable.defs.some(
def =>
def.type === 'ImportBinding' ||
def.type === 'ImplicitGlobalVariable' ||
(def.node && def.node.type === 'Program')
)
) {
return;
}
const declarationScope = variable.scope;
const uniqueFunctionScopes = new Set();
let allReferencesInsideFunctions = true;
let hasNonWriteReferences = false;
// Analyze all references to this variable
for (const reference of variable.references) {
// Skip the initial declaration
if (reference.init) {
continue;
}
hasNonWriteReferences = true;
const referenceScope = sourceCode.getScope(
reference.identifier
);
const containingFunctionScope = findContainingFunctionScope(
referenceScope,
declarationScope
);
if (containingFunctionScope) {
uniqueFunctionScopes.add(containingFunctionScope);
} else {
// Reference is not inside a function (used at module/global level)
allReferencesInsideFunctions = false;
break;
}
}
// Only suggest moving if:
// 1. All references are inside functions
// 2. All references are inside the same single function
// 3. There are actual non-write references
// 4. The containing function is a direct child of the declaration scope
if (
allReferencesInsideFunctions &&
uniqueFunctionScopes.size === 1 &&
hasNonWriteReferences
) {
const singleFunctionScope =
Array.from(uniqueFunctionScopes)[0];
// Check if the function scope is a direct child of the declaration scope
if (singleFunctionScope.upper === declarationScope) {
const variableNameNode = variable.defs[0].name;
const functionNode = singleFunctionScope.block;
const usageScopeIdentifier =
functionInfoForNarrowestScope.get(functionNode) ||
'[anonymous_function]';
context.report({
node: variableNameNode,
messageId: 'moveToNarrowerScope',
data: {
variableName: variable.name,
declarationScopeType: declarationScope.type,
usageScopeType: singleFunctionScope.type,
usageScopeIdentifier: usageScopeIdentifier,
},
});
}
}
});
});
},
};
},
},
},
};