"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const ts = __importStar(require("typescript")); const util = __importStar(require("../util")); const utils_1 = require("@typescript-eslint/utils"); const tsutils_1 = require("tsutils"); /* The AST is always constructed such the first element is always the deepest element. I.e. for this code: `foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz` The AST will look like this: { left: { left: { left: foo right: foo.bar } right: foo.bar.baz } right: foo.bar.baz.buzz } */ exports.default = util.createRule({ name: 'prefer-optional-chain', meta: { type: 'suggestion', docs: { description: 'Enforce using concise optional chain expressions instead of chained logical ands', recommended: 'strict', suggestion: true, }, hasSuggestions: true, messages: { preferOptionalChain: "Prefer using an optional chain expression instead, as it's more concise and easier to read.", optionalChainSuggest: 'Change to an optional chain.', }, schema: [], }, defaultOptions: [], create(context) { const sourceCode = context.getSourceCode(); const parserServices = util.getParserServices(context, true); return { 'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'(node) { const leftNode = node.left; const rightNode = node.right; const parentNode = node.parent; const isRightNodeAnEmptyObjectLiteral = rightNode.type === utils_1.AST_NODE_TYPES.ObjectExpression && rightNode.properties.length === 0; if (!isRightNodeAnEmptyObjectLiteral || !parentNode || parentNode.type !== utils_1.AST_NODE_TYPES.MemberExpression || parentNode.optional) { return; } function isLeftSideLowerPrecedence() { const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode); const operator = (0, tsutils_1.isBinaryExpression)(logicalTsNode) ? logicalTsNode.operatorToken.kind : ts.SyntaxKind.Unknown; const leftPrecedence = util.getOperatorPrecedence(leftTsNode.kind, operator); return leftPrecedence < util.OperatorPrecedence.LeftHandSide; } context.report({ node: parentNode, messageId: 'optionalChainSuggest', suggest: [ { messageId: 'optionalChainSuggest', fix: (fixer) => { const leftNodeText = sourceCode.getText(leftNode); // Any node that is made of an operator with higher or equal precedence, const maybeWrappedLeftNode = isLeftSideLowerPrecedence() ? `(${leftNodeText})` : leftNodeText; const propertyToBeOptionalText = sourceCode.getText(parentNode.property); const maybeWrappedProperty = parentNode.computed ? `[${propertyToBeOptionalText}]` : propertyToBeOptionalText; return fixer.replaceTextRange(parentNode.range, `${maybeWrappedLeftNode}?.${maybeWrappedProperty}`); }, }, ], }); }, [[ 'LogicalExpression[operator="&&"] > Identifier', 'LogicalExpression[operator="&&"] > MemberExpression', 'LogicalExpression[operator="&&"] > ChainExpression > MemberExpression', 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]', 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]', ].join(',')](initialIdentifierOrNotEqualsExpr) { var _a; // selector guarantees this cast const initialExpression = (((_a = initialIdentifierOrNotEqualsExpr.parent) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.ChainExpression ? initialIdentifierOrNotEqualsExpr.parent.parent : initialIdentifierOrNotEqualsExpr.parent); if (initialExpression.left !== initialIdentifierOrNotEqualsExpr) { // the node(identifier or member expression) is not the deepest left node return; } if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { return; } // walk up the tree to figure out how many logical expressions we can include let previous = initialExpression; let current = initialExpression; let previousLeftText = getText(initialIdentifierOrNotEqualsExpr); let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === utils_1.AST_NODE_TYPES.LogicalExpression) { if (!isValidChainTarget(current.right, // only allow identifiers for the first chain - foo && foo() expressionCount === 1)) { break; } const leftText = previousLeftText; const rightText = getText(current.right); // can't just use startsWith because of cases like foo && fooBar.baz; const matchRegex = new RegExp(`^${ // escape regex characters leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^a-zA-Z0-9_$]`); if (!matchRegex.test(rightText) && // handle redundant cases like foo.bar && foo.bar leftText !== rightText) { break; } // omit weird doubled up expression that make no sense like foo.bar && foo.bar if (rightText !== leftText) { expressionCount += 1; previousLeftText = rightText; /* Diff the left and right text to construct the fix string There are the following cases: 1) rightText === 'foo.bar.baz.buzz' leftText === 'foo.bar.baz' diff === '.buzz' 2) rightText === 'foo.bar.baz.buzz()' leftText === 'foo.bar.baz' diff === '.buzz()' 3) rightText === 'foo.bar.baz.buzz()' leftText === 'foo.bar.baz.buzz' diff === '()' 4) rightText === 'foo.bar.baz[buzz]' leftText === 'foo.bar.baz' diff === '[buzz]' 5) rightText === 'foo.bar.baz?.buzz' leftText === 'foo.bar.baz' diff === '?.buzz' */ const diff = rightText.replace(leftText, ''); if (diff.startsWith('?')) { // item was "pre optional chained" optionallyChainedCode += diff; } else { const needsDot = diff.startsWith('(') || diff.startsWith('['); optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; } } previous = current; current = util.nullThrows(current.parent, util.NullThrowsReasons.MissingParent); } if (expressionCount > 1) { if (previous.right.type === utils_1.AST_NODE_TYPES.BinaryExpression) { // case like foo && foo.bar !== someValue optionallyChainedCode += ` ${previous.right.operator} ${sourceCode.getText(previous.right.right)}`; } context.report({ node: previous, messageId: 'preferOptionalChain', suggest: [ { messageId: 'optionalChainSuggest', fix: (fixer) => [ fixer.replaceText(previous, optionallyChainedCode), ], }, ], }); } }, }; function getText(node) { if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) { return getText( // isValidChainTarget ensures this is type safe node.left); } if (node.type === utils_1.AST_NODE_TYPES.CallExpression) { const calleeText = getText( // isValidChainTarget ensures this is type safe node.callee); // ensure that the call arguments are left untouched, or else we can break cases that _need_ whitespace: // - JSX: // - Unary Operators: typeof foo, await bar, delete baz const closingParenToken = util.nullThrows(sourceCode.getLastToken(node), util.NullThrowsReasons.MissingToken('closing parenthesis', node.type)); const openingParenToken = util.nullThrows(sourceCode.getFirstTokenBetween(node.callee, closingParenToken, util.isOpeningParenToken), util.NullThrowsReasons.MissingToken('opening parenthesis', node.type)); const argumentsText = sourceCode.text.substring(openingParenToken.range[0], closingParenToken.range[1]); return `${calleeText}${argumentsText}`; } if (node.type === utils_1.AST_NODE_TYPES.Identifier) { return node.name; } if (node.type === utils_1.AST_NODE_TYPES.ThisExpression) { return 'this'; } if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) { /* istanbul ignore if */ if (node.expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) { // this shouldn't happen return ''; } return getText(node.expression); } return getMemberExpressionText(node); } /** * Gets a normalized representation of the given MemberExpression */ function getMemberExpressionText(node) { let objectText; // cases should match the list in ALLOWED_MEMBER_OBJECT_TYPES switch (node.object.type) { case utils_1.AST_NODE_TYPES.CallExpression: case utils_1.AST_NODE_TYPES.Identifier: objectText = getText(node.object); break; case utils_1.AST_NODE_TYPES.MemberExpression: objectText = getMemberExpressionText(node.object); break; case utils_1.AST_NODE_TYPES.ThisExpression: objectText = getText(node.object); break; /* istanbul ignore next */ default: throw new Error(`Unexpected member object type: ${node.object.type}`); } let propertyText; if (node.computed) { // cases should match the list in ALLOWED_COMPUTED_PROP_TYPES switch (node.property.type) { case utils_1.AST_NODE_TYPES.Identifier: propertyText = getText(node.property); break; case utils_1.AST_NODE_TYPES.Literal: case utils_1.AST_NODE_TYPES.TemplateLiteral: propertyText = sourceCode.getText(node.property); break; case utils_1.AST_NODE_TYPES.MemberExpression: propertyText = getMemberExpressionText(node.property); break; /* istanbul ignore next */ default: throw new Error(`Unexpected member property type: ${node.object.type}`); } return `${objectText}${node.optional ? '?.' : ''}[${propertyText}]`; } else { // cases should match the list in ALLOWED_NON_COMPUTED_PROP_TYPES switch (node.property.type) { case utils_1.AST_NODE_TYPES.Identifier: propertyText = getText(node.property); break; /* istanbul ignore next */ default: throw new Error(`Unexpected member property type: ${node.object.type}`); } return `${objectText}${node.optional ? '?.' : '.'}${propertyText}`; } } }, }); const ALLOWED_MEMBER_OBJECT_TYPES = new Set([ utils_1.AST_NODE_TYPES.CallExpression, utils_1.AST_NODE_TYPES.Identifier, utils_1.AST_NODE_TYPES.MemberExpression, utils_1.AST_NODE_TYPES.ThisExpression, ]); const ALLOWED_COMPUTED_PROP_TYPES = new Set([ utils_1.AST_NODE_TYPES.Identifier, utils_1.AST_NODE_TYPES.Literal, utils_1.AST_NODE_TYPES.MemberExpression, utils_1.AST_NODE_TYPES.TemplateLiteral, ]); const ALLOWED_NON_COMPUTED_PROP_TYPES = new Set([ utils_1.AST_NODE_TYPES.Identifier, ]); function isValidChainTarget(node, allowIdentifier) { if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) { return isValidChainTarget(node.expression, allowIdentifier); } if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) { const isObjectValid = ALLOWED_MEMBER_OBJECT_TYPES.has(node.object.type) && // make sure to validate the expression is of our expected structure isValidChainTarget(node.object, true); const isPropertyValid = node.computed ? ALLOWED_COMPUTED_PROP_TYPES.has(node.property.type) && // make sure to validate the member expression is of our expected structure (node.property.type === utils_1.AST_NODE_TYPES.MemberExpression ? isValidChainTarget(node.property, allowIdentifier) : true) : ALLOWED_NON_COMPUTED_PROP_TYPES.has(node.property.type); return isObjectValid && isPropertyValid; } if (node.type === utils_1.AST_NODE_TYPES.CallExpression) { return isValidChainTarget(node.callee, allowIdentifier); } if (allowIdentifier && (node.type === utils_1.AST_NODE_TYPES.Identifier || node.type === utils_1.AST_NODE_TYPES.ThisExpression)) { return true; } /* special case for the following, where we only want the left - foo !== null - foo != null - foo !== undefined - foo != undefined */ return (node.type === utils_1.AST_NODE_TYPES.BinaryExpression && ['!==', '!='].includes(node.operator) && isValidChainTarget(node.left, allowIdentifier) && (util.isUndefinedIdentifier(node.right) || util.isNullLiteral(node.right))); } //# sourceMappingURL=prefer-optional-chain.js.map