| "use strict"; |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| if (k2 === undefined) k2 = k; |
| Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); |
| }) : (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 experimental_utils_1 = require("@typescript-eslint/experimental-utils"); |
| const util = __importStar(require("../util")); |
| /* |
| 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: 'Prefer using concise optional chain expressions instead of chained logical ands', |
| category: 'Best Practices', |
| recommended: false, |
| suggestion: 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(); |
| return { |
| [[ |
| '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) === experimental_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 === experimental_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 === experimental_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 === experimental_utils_1.AST_NODE_TYPES.BinaryExpression) { |
| return getText( |
| // isValidChainTarget ensures this is type safe |
| node.left); |
| } |
| if (node.type === experimental_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: <Foo Needs Space Between Attrs /> |
| // - 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 === experimental_utils_1.AST_NODE_TYPES.Identifier) { |
| return node.name; |
| } |
| if (node.type === experimental_utils_1.AST_NODE_TYPES.ThisExpression) { |
| return 'this'; |
| } |
| if (node.type === experimental_utils_1.AST_NODE_TYPES.ChainExpression) { |
| /* istanbul ignore if */ if (node.expression.type === experimental_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 experimental_utils_1.AST_NODE_TYPES.CallExpression: |
| case experimental_utils_1.AST_NODE_TYPES.Identifier: |
| objectText = getText(node.object); |
| break; |
| case experimental_utils_1.AST_NODE_TYPES.MemberExpression: |
| objectText = getMemberExpressionText(node.object); |
| break; |
| case experimental_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 experimental_utils_1.AST_NODE_TYPES.Identifier: |
| propertyText = getText(node.property); |
| break; |
| case experimental_utils_1.AST_NODE_TYPES.Literal: |
| case experimental_utils_1.AST_NODE_TYPES.TemplateLiteral: |
| propertyText = sourceCode.getText(node.property); |
| break; |
| case experimental_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 experimental_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([ |
| experimental_utils_1.AST_NODE_TYPES.CallExpression, |
| experimental_utils_1.AST_NODE_TYPES.Identifier, |
| experimental_utils_1.AST_NODE_TYPES.MemberExpression, |
| experimental_utils_1.AST_NODE_TYPES.ThisExpression, |
| ]); |
| const ALLOWED_COMPUTED_PROP_TYPES = new Set([ |
| experimental_utils_1.AST_NODE_TYPES.Identifier, |
| experimental_utils_1.AST_NODE_TYPES.Literal, |
| experimental_utils_1.AST_NODE_TYPES.MemberExpression, |
| experimental_utils_1.AST_NODE_TYPES.TemplateLiteral, |
| ]); |
| const ALLOWED_NON_COMPUTED_PROP_TYPES = new Set([ |
| experimental_utils_1.AST_NODE_TYPES.Identifier, |
| ]); |
| function isValidChainTarget(node, allowIdentifier) { |
| if (node.type === experimental_utils_1.AST_NODE_TYPES.ChainExpression) { |
| return isValidChainTarget(node.expression, allowIdentifier); |
| } |
| if (node.type === experimental_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 === experimental_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 === experimental_utils_1.AST_NODE_TYPES.CallExpression) { |
| return isValidChainTarget(node.callee, allowIdentifier); |
| } |
| if (allowIdentifier && |
| (node.type === experimental_utils_1.AST_NODE_TYPES.Identifier || |
| node.type === experimental_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 |
| */ |
| if (node.type === experimental_utils_1.AST_NODE_TYPES.BinaryExpression && |
| ['!==', '!='].includes(node.operator) && |
| isValidChainTarget(node.left, allowIdentifier)) { |
| if (node.right.type === experimental_utils_1.AST_NODE_TYPES.Identifier && |
| node.right.name === 'undefined') { |
| return true; |
| } |
| if (node.right.type === experimental_utils_1.AST_NODE_TYPES.Literal && |
| node.right.value === null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| //# sourceMappingURL=prefer-optional-chain.js.map |