UNPKG

eslint-plugin-unicorn

Version:
111 lines (92 loc) • 2.45 kB
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;