From f764abe32a621e6018dff11c16a0f4ce6cc9f07f Mon Sep 17 00:00:00 2001 From: Jakob Guddas Date: Thu, 19 May 2022 16:22:34 +0200 Subject: [PATCH] feat(eslint-plugin): added support for MemberExpressions for ignoreTernaryTests option --- .../src/rules/prefer-nullish-coalescing.ts | 107 ++++++++++++++---- .../rules/prefer-nullish-coalescing.test.ts | 63 +++++++---- 2 files changed, 131 insertions(+), 39 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 3a5b49edbf1a..a94090787f22 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -87,11 +87,12 @@ export default util.createRule({ return; } - let identifier: TSESTree.Identifier; + let identifier: TSESTree.Identifier | TSESTree.MemberExpression; let alternate: TSESTree.Expression; let requiredOperator: '!==' | '==='; if ( - node.consequent.type === AST_NODE_TYPES.Identifier && + (node.consequent.type === AST_NODE_TYPES.Identifier || + node.consequent.type === AST_NODE_TYPES.MemberExpression) && ((node.test.type === AST_NODE_TYPES.BinaryExpression && (node.test.operator === '!==' || node.test.operator === '!=')) || (node.test.type === AST_NODE_TYPES.LogicalExpression && @@ -102,7 +103,10 @@ export default util.createRule({ identifier = node.consequent; alternate = node.alternate; requiredOperator = '!=='; - } else if (node.alternate.type === AST_NODE_TYPES.Identifier) { + } else if ( + node.alternate.type === AST_NODE_TYPES.Identifier || + node.alternate.type === AST_NODE_TYPES.MemberExpression + ) { identifier = node.alternate; alternate = node.consequent; requiredOperator = '==='; @@ -138,7 +142,10 @@ export default util.createRule({ fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { return fixer.replaceText( node, - `${identifier.name} ?? ${sourceCode.text.slice( + `${sourceCode.text.slice( + identifier.range[0], + identifier.range[1], + )} ?? ${sourceCode.text.slice( alternate.range[0], alternate.range[1], )}`, @@ -282,7 +289,7 @@ function isFixableExplicitTernary({ node, }: { requiredOperator: '!==' | '==='; - identifier: TSESTree.Identifier; + identifier: TSESTree.Identifier | TSESTree.MemberExpression; node: TSESTree.ConditionalExpression; }): boolean { if (node.test.type !== AST_NODE_TYPES.LogicalExpression) { @@ -306,10 +313,7 @@ function isFixableExplicitTernary({ return false; } - const isIdentifier = ( - i: TSESTree.Expression | TSESTree.PrivateIdentifier, - ): boolean => - i.type === AST_NODE_TYPES.Identifier && i.name === identifier.name; + const isIdentifier = isEqualIndentierCurry(identifier); const hasUndefinedCheck = (isIdentifier(left.left) && isUndefined(left.right)) || @@ -340,7 +344,7 @@ function isFixableLooseTernary({ requiredOperator, }: { requiredOperator: '!==' | '==='; - identifier: TSESTree.Identifier; + identifier: TSESTree.Identifier | TSESTree.MemberExpression; node: TSESTree.ConditionalExpression; }): boolean { if (node.test.type !== AST_NODE_TYPES.BinaryExpression) { @@ -355,10 +359,7 @@ function isFixableLooseTernary({ return false; } - const isIdentifier = ( - i: TSESTree.Expression | TSESTree.PrivateIdentifier, - ): boolean => - i.type === AST_NODE_TYPES.Identifier && i.name === identifier.name; + const isIdentifier = isEqualIndentierCurry(identifier); if (isIdentifier(right) && (isNull(left) || isUndefined(left))) { return true; @@ -387,7 +388,7 @@ function isFixableImplicitTernary({ parserServices: ReturnType; checker: ts.TypeChecker; requiredOperator: '!==' | '==='; - identifier: TSESTree.Identifier; + identifier: TSESTree.Identifier | TSESTree.MemberExpression; node: TSESTree.ConditionalExpression; }): boolean { if (node.test.type !== AST_NODE_TYPES.BinaryExpression) { @@ -397,10 +398,7 @@ function isFixableImplicitTernary({ if (operator !== requiredOperator) { return false; } - const isIdentifier = ( - i: TSESTree.Expression | TSESTree.PrivateIdentifier, - ): boolean => - i.type === AST_NODE_TYPES.Identifier && i.name === identifier.name; + const isIdentifier = isEqualIndentierCurry(identifier); const i = isIdentifier(left) ? left : isIdentifier(right) ? right : null; if (!i) { @@ -436,6 +434,77 @@ function isFixableImplicitTernary({ return false; } +function isEqualIndentierCurry( + a: TSESTree.Identifier | TSESTree.MemberExpression, +) { + if (a.type === AST_NODE_TYPES.Identifier) { + return function (b: any): boolean { + if (a.type !== b.type) { + return false; + } + return !!a.name && !!b.name && a.name === b.name; + }; + } + return function (b: any): boolean { + if (a.type !== b.type) { + return false; + } + return isEqualMemberExpression(a, b); + }; +} +function isEqualMemberExpression( + a: TSESTree.MemberExpression, + b: TSESTree.MemberExpression, +): boolean { + return ( + isEqualMemberExpressionProperty(a.property, b.property) && + isEqualMemberExpressionObject(a.object, b.object) + ); +} + +function isEqualMemberExpressionProperty( + a: TSESTree.MemberExpression['property'], + b: TSESTree.MemberExpression['property'], +): boolean { + if (a.type !== b.type) { + return false; + } + if (a.type === AST_NODE_TYPES.ThisExpression) { + return true; + } + if ( + a.type === AST_NODE_TYPES.Literal || + a.type === AST_NODE_TYPES.Identifier + ) { + return ( + // @ts-ignore + (!!a.name && !!b.name && a.name === b.name) || + // @ts-ignore + (!!a.value && !!b.value && a.value === b.value) + ); + } + if (a.type === AST_NODE_TYPES.MemberExpression) { + return isEqualMemberExpression(a, b as typeof a); + } + return false; +} + +function isEqualMemberExpressionObject(a: any, b: any): boolean { + if (a.type !== b.type) { + return false; + } + if (a.type === AST_NODE_TYPES.ThisExpression) { + return true; + } + if (a.type === AST_NODE_TYPES.Identifier) { + return a.name === b.name; + } + if (a.type === AST_NODE_TYPES.MemberExpression) { + return isEqualMemberExpression(a, b); + } + return false; +} + function isUndefined( i: TSESTree.Expression | TSESTree.PrivateIdentifier, ): boolean { diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 3f061ffd8573..9ad3be9a3ce9 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -244,26 +244,49 @@ x ?? 'foo'; 'x != null ? x : y;', 'x == undefined ? y : x;', 'x == null ? y : x;', - ].map(code => ({ - code, - output: null, - options: [{ ignoreTernaryTests: false }] as const, - errors: [ - { - messageId: 'preferNullishOverTernary' as const, - line: 1, - column: 1, - endLine: 1, - endColumn: code.length, - suggestions: [ - { - messageId: 'suggestNullish' as const, - output: 'x ?? y;', - }, - ], - }, - ], - })), + ].flatMap(code => [ + { + code, + output: null, + options: [{ ignoreTernaryTests: false }] as const, + errors: [ + { + messageId: 'preferNullishOverTernary' as const, + line: 1, + column: 1, + endLine: 1, + endColumn: code.length, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: 'x ?? y;', + }, + ], + }, + ], + }, + { + code: code.replace(/x/g, 'x.z[1][this[this.o]]["3"][a.b.c]'), + output: null, + options: [{ ignoreTernaryTests: false }] as const, + errors: [ + { + messageId: 'preferNullishOverTernary' as const, + line: 1, + column: 1, + endLine: 1, + endColumn: code.replace(/x/g, 'x.z[1][this[this.o]]["3"][a.b.c]') + .length, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: 'x.z[1][this[this.o]]["3"][a.b.c] ?? y;', + }, + ], + }, + ], + }, + ]), ...[ `