Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [prefer-optional-chain] support logical with empty object #4430

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a5d7ec7
feat(eslint-plugin): issue 4395
omril1 Jan 10, 2022
a1dcc4d
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 10, 2022
a5823d3
feat(eslint-plugin): issue 4395
omril1 Jan 11, 2022
65392a8
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 11, 2022
c788fcb
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 13, 2022
bd59eb5
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 15, 2022
6002a5c
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 19, 2022
aac5eb1
fix(eslint-plugin): cr comment
omril1 Jan 19, 2022
155e936
fix(eslint-plugin): more UT and handle ternary
omril1 Jan 24, 2022
13f6e95
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 24, 2022
7f907c5
fix(eslint-plugin): remove comment
omril1 Jan 24, 2022
d9c8120
fix(eslint-plugin): prefer optional chaining over `?? {}).`
omril1 Jan 26, 2022
4065bfe
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Jan 31, 2022
3249b85
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 2, 2022
914d2f9
Legit nitpick
omril1 Feb 9, 2022
6836a74
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 9, 2022
d1650b0
fix(eslint-plugin): prefer optional chaining
omril1 Feb 10, 2022
f355e92
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 12, 2022
75884fb
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 12, 2022
aa33afb
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 13, 2022
3dc29b3
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Feb 17, 2022
2e06f7a
Merge branch 'main' into feat/issue-4395-empty-object-optional-chaining
omril1 Mar 18, 2022
44236bb
feat(eslint-plugin): cr util.getOperatorPrecedence
omril1 Mar 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
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