eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
111 lines (92 loc) • 2.45 kB
JavaScript
import {
isTypeOnlyDefinition,
unwrapChainAndTypeScriptExpression,
} from './utils/index.js';
const MESSAGE_ID = 'no-optional-chaining-on-undeclared-variable';
const messages = {
[MESSAGE_ID]: 'Optional chaining on undeclared variable `{{name}}` throws a ReferenceError.',
};
function getLeftmostMemberBase(node) {
node = unwrapChainAndTypeScriptExpression(node);
while (node.type === 'MemberExpression') {
node = unwrapChainAndTypeScriptExpression(node.object);
}
return node.type === 'Identifier' ? node : undefined;
}
function isUnresolvedRuntimeVariable(node, context) {
let scope = context.sourceCode.getScope(node);
while (scope) {
const variable = scope.set.get(node.name);
if (
variable
&& (
variable.defs.length === 0
|| variable.defs.some(definition => !isTypeOnlyDefinition(definition))
)
) {
return false;
}
scope = scope.upper;
}
return true;
}
function getOptionalOperationBase(node) {
node = unwrapChainAndTypeScriptExpression(node);
if (node.type === 'MemberExpression') {
return getOptionalOperationBase(node.object) ?? (node.optional ? node.object : undefined);
}
if (node.type === 'CallExpression') {
return getOptionalOperationBase(node.callee) ?? (node.optional ? node.callee : undefined);
}
}
function getUndeclaredOptionalChainBase(chainExpression, context) {
// A `ChainExpression` always has an optional link along its object/callee path, so there is always a base.
const base = getOptionalOperationBase(chainExpression.expression);
const identifier = getLeftmostMemberBase(base);
if (
identifier
&& isUnresolvedRuntimeVariable(identifier, context)
) {
return identifier;
}
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
const reportedIdentifiers = new WeakSet();
context.on('ChainExpression', node => {
const identifier = getUndeclaredOptionalChainBase(node, context);
if (
!identifier
|| reportedIdentifiers.has(identifier)
) {
return;
}
reportedIdentifiers.add(identifier);
return {
node: identifier,
messageId: MESSAGE_ID,
data: {name: identifier.name},
};
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow optional chaining on undeclared variables.',
recommended: true,
},
schema: [],
messages,
languages: [
'js/js',
],
},
};
export default config;