Skip to content

Commit

Permalink
feat(eslint-plugin): [prefer-optional-chain] support logical with emp…
Browse files Browse the repository at this point in the history
…ty object (#4430)
  • Loading branch information
omril1 committed Mar 18, 2022
1 parent 8ec05be commit d21cfe0
Show file tree
Hide file tree
Showing 4 changed files with 714 additions and 1 deletion.
7 changes: 7 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-optional-chain.md
Expand Up @@ -22,6 +22,10 @@ function myFunc(foo: T | null) {
function myFunc(foo: T | null) {
return foo && foo.a && foo.a.b && foo.a.b.c;
}
// or
function myFunc(foo: T | null) {
return (((foo || {}).a || {}).b || {}).c;
}

function myFunc(foo: T | null) {
return foo?.['a']?.b?.c;
Expand Down Expand Up @@ -57,6 +61,9 @@ foo && foo.a && foo.a.b && foo.a.b.c;
foo && foo['a'] && foo['a'].b && foo['a'].b.c;
foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method();

(((foo || {}).a || {}).b {}).c;
(((foo || {})['a'] || {}).b {}).c;

// this rule also supports converting chained strict nullish checks:
foo &&
foo.a != null &&
Expand Down
65 changes: 64 additions & 1 deletion packages/eslint-plugin/src/rules/prefer-optional-chain.ts
@@ -1,5 +1,7 @@
import { AST_NODE_TYPES, TSESTree, TSESLint } from '@typescript-eslint/utils';
import * as ts from 'typescript';
import * as util from '../util';
import { AST_NODE_TYPES, TSESTree, TSESLint } from '@typescript-eslint/utils';
import { isBinaryExpression } from 'tsutils';

type ValidChainTarget =
| TSESTree.BinaryExpression
Expand Down Expand Up @@ -47,7 +49,68 @@ export default util.createRule({
defaultOptions: [],
create(context) {
const sourceCode = context.getSourceCode();
const parserServices = util.getParserServices(context, true);

return {
'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'(
node: TSESTree.LogicalExpression,
): void {
const leftNode = node.left;
const rightNode = node.right;
const parentNode = node.parent;
const isRightNodeAnEmptyObjectLiteral =
rightNode.type === AST_NODE_TYPES.ObjectExpression &&
rightNode.properties.length === 0;
if (
!isRightNodeAnEmptyObjectLiteral ||
!parentNode ||
parentNode.type !== AST_NODE_TYPES.MemberExpression ||
parentNode.optional
) {
return;
}

function isLeftSideLowerPrecedence(): boolean {
const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node);

const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode);
const operator = 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): TSESLint.RuleFix => {
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',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/index.ts
Expand Up @@ -4,6 +4,7 @@ export * from './astUtils';
export * from './collectUnusedVariables';
export * from './createRule';
export * from './getFunctionHeadLoc';
export * from './getOperatorPrecedence';
export * from './getThisExpression';
export * from './getWrappingFixer';
export * from './misc';
Expand Down

0 comments on commit d21cfe0

Please sign in to comment.