diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index 5e47975ad3ff..c82399072ddc 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -46,6 +46,7 @@ This rule aims enforce the usage of the safer operator. ```ts type Options = [ { + ignoreTernaryTests?: boolean; ignoreConditionalTests?: boolean; ignoreMixedLogicalExpressions?: boolean; }, @@ -53,12 +54,53 @@ type Options = [ const defaultOptions = [ { + ignoreTernaryTests: true; ignoreConditionalTests: true, ignoreMixedLogicalExpressions: true, }, ]; ``` +### `ignoreTernaryTests` + +Setting this option to `true` (the default) will cause the rule to ignore any ternary expressions that could be simplified by using the nullish coalescing operator. + +Incorrect code for `ignoreTernaryTests: false`, and correct code for `ignoreTernaryTests: true`: + +```ts +const foo: any = 'bar'; +foo !== undefined && foo !== null ? foo : 'a string'; +foo === undefined || foo === null ? 'a string' : foo; +foo == undefined ? 'a string' : foo; +foo == null ? 'a string' : foo; + +const foo: string | undefined = 'bar'; +foo !== undefined ? foo : 'a string'; +foo === undefined ? 'a string' : foo; + +const foo: string | null = 'bar'; +foo !== null ? foo : 'a string'; +foo === null ? 'a string' : foo; +``` + +Correct code for `ignoreTernaryTests: false`: + +```ts +const foo: any = 'bar'; +foo ?? 'a string'; +foo ?? 'a string'; +foo ?? 'a string'; +foo ?? 'a string'; + +const foo: string | undefined = 'bar'; +foo ?? 'a string'; +foo ?? 'a string'; + +const foo: string | null = 'bar'; +foo ?? 'a string'; +foo ?? 'a string'; +``` + ### `ignoreConditionalTests` Setting this option to `true` (the default) will cause the rule to ignore any cases that are located within a conditional test. diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index c1676d3fd323..b2321fd07570 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -5,14 +5,20 @@ import { TSESTree, } from '@typescript-eslint/utils'; import * as util from '../util'; +import * as ts from 'typescript'; export type Options = [ { ignoreConditionalTests?: boolean; + ignoreTernaryTests?: boolean; ignoreMixedLogicalExpressions?: boolean; }, ]; -export type MessageIds = 'preferNullish' | 'suggestNullish'; + +export type MessageIds = + | 'preferNullishOverOr' + | 'preferNullishOverTernary' + | 'suggestNullish'; export default util.createRule({ name: 'prefer-nullish-coalescing', @@ -27,8 +33,10 @@ export default util.createRule({ }, hasSuggestions: true, messages: { - preferNullish: + preferNullishOverOr: 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', + preferNullishOverTernary: + 'Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.', suggestNullish: 'Fix to nullish coalescing operator (`??`).', }, schema: [ @@ -38,6 +46,9 @@ export default util.createRule({ ignoreConditionalTests: { type: 'boolean', }, + ignoreTernaryTests: { + type: 'boolean', + }, ignoreMixedLogicalExpressions: { type: 'boolean', }, @@ -52,15 +63,182 @@ export default util.createRule({ defaultOptions: [ { ignoreConditionalTests: true, + ignoreTernaryTests: true, ignoreMixedLogicalExpressions: true, }, ], - create(context, [{ ignoreConditionalTests, ignoreMixedLogicalExpressions }]) { + create( + context, + [ + { + ignoreConditionalTests, + ignoreTernaryTests, + ignoreMixedLogicalExpressions, + }, + ], + ) { const parserServices = util.getParserServices(context); const sourceCode = context.getSourceCode(); const checker = parserServices.program.getTypeChecker(); return { + ConditionalExpression(node: TSESTree.ConditionalExpression): void { + if (ignoreTernaryTests) { + return; + } + + let operator: '==' | '!=' | '===' | '!==' | undefined; + let nodesInsideTestExpression: TSESTree.Node[] = []; + if (node.test.type === AST_NODE_TYPES.BinaryExpression) { + nodesInsideTestExpression = [node.test.left, node.test.right]; + if ( + node.test.operator === '==' || + node.test.operator === '!=' || + node.test.operator === '===' || + node.test.operator === '!==' + ) { + operator = node.test.operator; + } + } else if ( + node.test.type === AST_NODE_TYPES.LogicalExpression && + node.test.left.type === AST_NODE_TYPES.BinaryExpression && + node.test.right.type === AST_NODE_TYPES.BinaryExpression + ) { + nodesInsideTestExpression = [ + node.test.left.left, + node.test.left.right, + node.test.right.left, + node.test.right.right, + ]; + if (node.test.operator === '||') { + if ( + node.test.left.operator === '===' && + node.test.right.operator === '===' + ) { + operator = '==='; + } else if ( + ((node.test.left.operator === '===' || + node.test.right.operator === '===') && + (node.test.left.operator === '==' || + node.test.right.operator === '==')) || + (node.test.left.operator === '==' && + node.test.right.operator === '==') + ) { + operator = '=='; + } + } else if (node.test.operator === '&&') { + if ( + node.test.left.operator === '!==' && + node.test.right.operator === '!==' + ) { + operator = '!=='; + } else if ( + ((node.test.left.operator === '!==' || + node.test.right.operator === '!==') && + (node.test.left.operator === '!=' || + node.test.right.operator === '!=')) || + (node.test.left.operator === '!=' && + node.test.right.operator === '!=') + ) { + operator = '!='; + } + } + } + + if (!operator) { + return; + } + + let identifier: TSESTree.Node | undefined; + let hasUndefinedCheck = false; + let hasNullCheck = false; + + // we check that the test only contains null, undefined and the identifier + for (const testNode of nodesInsideTestExpression) { + if (util.isNullLiteral(testNode)) { + hasNullCheck = true; + } else if (util.isUndefinedIdentifier(testNode)) { + hasUndefinedCheck = true; + } else if ( + (operator === '!==' || operator === '!=') && + util.isNodeEqual(testNode, node.consequent) + ) { + identifier = testNode; + } else if ( + (operator === '===' || operator === '==') && + util.isNodeEqual(testNode, node.alternate) + ) { + identifier = testNode; + } else { + return; + } + } + + if (!identifier) { + return; + } + + const isFixable = ((): boolean => { + // it is fixable if we check for both null and undefined, or not if neither + if (hasUndefinedCheck === hasNullCheck) { + return hasUndefinedCheck; + } + + // it is fixable if we loosely check for either null or undefined + if (operator === '==' || operator === '!=') { + return true; + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(identifier); + const type = checker.getTypeAtLocation(tsNode); + const flags = util.getTypeFlags(type); + + if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + return false; + } + + const hasNullType = (flags & ts.TypeFlags.Null) !== 0; + + // it is fixable if we check for undefined and the type is not nullable + if (hasUndefinedCheck && !hasNullType) { + return true; + } + + const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0; + + // it is fixable if we check for null and the type can't be undefined + return hasNullCheck && !hasUndefinedType; + })(); + + if (isFixable) { + context.report({ + node, + messageId: 'preferNullishOverTernary', + suggest: [ + { + messageId: 'suggestNullish', + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + const [left, right] = + operator === '===' || operator === '==' + ? [node.alternate, node.consequent] + : [node.consequent, node.alternate]; + return fixer.replaceText( + node, + `${sourceCode.text.slice( + left.range[0], + left.range[1], + )} ?? ${sourceCode.text.slice( + right.range[0], + right.range[1], + )}`, + ); + }, + }, + ], + }); + } + }, + 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { @@ -110,7 +288,7 @@ export default util.createRule({ context.report({ node: barBarOperator, - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', suggest: [ { messageId: 'suggestNullish', diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 04cd7e649933..662439ac743a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -437,24 +437,10 @@ function isValidChainTarget( - foo !== undefined - foo != undefined */ - if ( + return ( node.type === AST_NODE_TYPES.BinaryExpression && ['!==', '!='].includes(node.operator) && - isValidChainTarget(node.left, allowIdentifier) - ) { - if ( - node.right.type === AST_NODE_TYPES.Identifier && - node.right.name === 'undefined' - ) { - return true; - } - if ( - node.right.type === AST_NODE_TYPES.Literal && - node.right.value === null - ) { - return true; - } - } - - return false; + isValidChainTarget(node.left, allowIdentifier) && + (util.isUndefinedIdentifier(node.right) || util.isNullLiteral(node.right)) + ); } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index b2932466388d..98ef1cf87076 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -9,6 +9,9 @@ export * from './getThisExpression'; export * from './getWrappingFixer'; export * from './misc'; export * from './objectIterators'; +export * from './isNullLiteral'; +export * from './isUndefinedIdentifier'; +export * from './isNodeEqual'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/isNodeEqual.ts b/packages/eslint-plugin/src/util/isNodeEqual.ts new file mode 100644 index 000000000000..ef879163ee44 --- /dev/null +++ b/packages/eslint-plugin/src/util/isNodeEqual.ts @@ -0,0 +1,31 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +export function isNodeEqual(a: TSESTree.Node, b: TSESTree.Node): boolean { + if (a.type !== b.type) { + return false; + } + if ( + a.type === AST_NODE_TYPES.ThisExpression && + b.type === AST_NODE_TYPES.ThisExpression + ) { + return true; + } + if (a.type === AST_NODE_TYPES.Literal && b.type === AST_NODE_TYPES.Literal) { + return a.value === b.value; + } + if ( + a.type === AST_NODE_TYPES.Identifier && + b.type === AST_NODE_TYPES.Identifier + ) { + return a.name === b.name; + } + if ( + a.type === AST_NODE_TYPES.MemberExpression && + b.type === AST_NODE_TYPES.MemberExpression + ) { + return ( + isNodeEqual(a.property, b.property) && isNodeEqual(a.object, b.object) + ); + } + return false; +} diff --git a/packages/eslint-plugin/src/util/isNullLiteral.ts b/packages/eslint-plugin/src/util/isNullLiteral.ts new file mode 100644 index 000000000000..e700e415f636 --- /dev/null +++ b/packages/eslint-plugin/src/util/isNullLiteral.ts @@ -0,0 +1,5 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +export function isNullLiteral(i: TSESTree.Node): boolean { + return i.type === AST_NODE_TYPES.Literal && i.value === null; +} diff --git a/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts b/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts new file mode 100644 index 000000000000..91cae07aa814 --- /dev/null +++ b/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts @@ -0,0 +1,5 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +export function isUndefinedIdentifier(i: TSESTree.Node): boolean { + return i.type === AST_NODE_TYPES.Identifier && i.name === 'undefined'; +} 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 8096df94c329..55fb4452973b 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -71,6 +71,71 @@ x ?? 'foo'; `, ), + { + code: 'x !== undefined && x !== null ? x : y;', + options: [{ ignoreTernaryTests: true }], + }, + + ...[ + 'x !== undefined && x !== null ? "foo" : "bar";', + 'x !== null && x !== undefined && x !== 5 ? x : y', + 'x === null || x === undefined || x === 5 ? x : y', + 'x === undefined && x !== null ? x : y;', + 'x === undefined && x === null ? x : y;', + 'x !== undefined && x === null ? x : y;', + 'x === undefined || x !== null ? x : y;', + 'x === undefined || x === null ? x : y;', + 'x !== undefined || x === null ? x : y;', + 'x !== undefined || x === null ? y : x;', + 'x === null || x === null ? y : x;', + 'x === undefined || x === undefined ? y : x;', + 'x == null ? x : y;', + 'undefined == null ? x : y;', + 'undefined != z ? x : y;', + 'x == undefined ? x : y;', + 'x != null ? y : x;', + 'x != undefined ? y : x;', + 'null == x ? x : y;', + 'undefined == x ? x : y;', + 'null != x ? y : x;', + 'undefined != x ? y : x;', + ` +declare const x: string; +x === null ? x : y; + `, + ` +declare const x: string | undefined; +x === null ? x : y; + `, + ` +declare const x: string | null; +x === undefined ? x : y; + `, + ` +declare const x: string | undefined | null; +x !== undefined ? x : y; + `, + ` +declare const x: string | undefined | null; +x !== null ? x : y; + `, + ` +declare const x: string | null | any; +x === null ? x : y; + `, + ` +declare const x: string | null | unknown; +x === null ? x : y; + `, + ` +declare const x: string | undefined; +x === null ? x : y; + `, + ].map(code => ({ + code, + options: [{ ignoreTernaryTests: false }] as const, + })), + // ignoreConditionalTests ...nullishTypeValidTest((nullish, type) => ({ code: ` @@ -148,7 +213,7 @@ x || 'foo'; output: null, errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 3, endLine: 3, @@ -166,6 +231,158 @@ x ?? 'foo'; ], })), + ...[ + 'x !== undefined && x !== null ? x : y;', + 'x !== null && x !== undefined ? x : y;', + 'x === undefined || x === null ? y : x;', + 'x === null || x === undefined ? y : x;', + 'undefined !== x && x !== null ? x : y;', + 'null !== x && x !== undefined ? x : y;', + 'undefined === x || x === null ? y : x;', + 'null === x || x === undefined ? y : x;', + 'x !== undefined && null !== x ? x : y;', + 'x !== null && undefined !== x ? x : y;', + 'x === undefined || null === x ? y : x;', + 'x === null || undefined === x ? y : x;', + 'undefined !== x && null !== x ? x : y;', + 'null !== x && undefined !== x ? x : y;', + 'undefined === x || null === x ? y : x;', + 'null === x || undefined === x ? y : x;', + 'x != undefined && x != null ? x : y;', + 'x == undefined || x == null ? y : x;', + 'x != undefined && x !== null ? x : y;', + 'x == undefined || x === null ? y : x;', + 'x !== undefined && x != null ? x : y;', + 'undefined != x ? x : y;', + 'null != x ? x : y;', + 'undefined == x ? y : x;', + 'null == x ? y : x;', + 'x != undefined ? x : y;', + 'x != null ? x : y;', + 'x == undefined ? y : x;', + 'x == null ? y : x;', + ].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;', + }, + ], + }, + ], + }, + ]), + + { + code: 'this != undefined ? this : y;', + output: null, + options: [{ ignoreTernaryTests: false }] as const, + errors: [ + { + messageId: 'preferNullishOverTernary' as const, + line: 1, + column: 1, + endLine: 1, + endColumn: 29, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: 'this ?? y;', + }, + ], + }, + ], + }, + + ...[ + ` +declare const x: string | undefined; +x !== undefined ? x : y; + `.trim(), + ` +declare const x: string | undefined; +undefined !== x ? x : y; + `.trim(), + ` +declare const x: string | undefined; +undefined === x ? y : x; + `.trim(), + ` +declare const x: string | undefined; +undefined === x ? y : x; + `.trim(), + ` +declare const x: string | null; +x !== null ? x : y; + `.trim(), + ` +declare const x: string | null; +null !== x ? x : y; + `.trim(), + ` +declare const x: string | null; +null === x ? y : x; + `.trim(), + ` +declare const x: string | null; +null === x ? y : x; + `.trim(), + ].map(code => ({ + code, + output: null, + options: [{ ignoreTernaryTests: false }] as const, + errors: [ + { + messageId: 'preferNullishOverTernary' as const, + line: 2, + column: 1, + endLine: 2, + endColumn: code.split('\n')[1].length, + suggestions: [ + { + messageId: 'suggestNullish' as const, + output: ` +${code.split('\n')[0]} +x ?? y; + `.trim(), + }, + ], + }, + ], + })), + // ignoreConditionalTests ...nullishTypeInvalidTest((nullish, type) => ({ code: ` @@ -176,7 +393,7 @@ x || 'foo' ? null : null; options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 3, endLine: 3, @@ -202,7 +419,7 @@ if (x || 'foo') {} options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 7, endLine: 3, @@ -228,7 +445,7 @@ do {} while (x || 'foo') options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 16, endLine: 3, @@ -254,7 +471,7 @@ for (;x || 'foo';) {} options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 9, endLine: 3, @@ -280,7 +497,7 @@ while (x || 'foo') {} options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 10, endLine: 3, @@ -309,7 +526,7 @@ a || b && c; options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 5, column: 3, endLine: 5, @@ -339,7 +556,7 @@ a || b || c && d; options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 6, column: 3, endLine: 6, @@ -358,7 +575,7 @@ declare const d: ${type} | ${nullish}; ], }, { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 6, column: 8, endLine: 6, @@ -389,7 +606,7 @@ a && b || c || d; options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 6, column: 8, endLine: 6, @@ -408,7 +625,7 @@ a && (b ?? c) || d; ], }, { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 6, column: 13, endLine: 6, @@ -439,7 +656,7 @@ if (() => x || 'foo') {} options: [{ ignoreConditionalTests: true }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 13, endLine: 3, @@ -465,7 +682,7 @@ if (function werid() { return x || 'foo' }) {} options: [{ ignoreConditionalTests: true }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 3, column: 33, endLine: 3, @@ -482,7 +699,6 @@ if (function werid() { return x ?? 'foo' }) {} }, ], })), - // https://github.com/typescript-eslint/typescript-eslint/issues/1290 ...nullishTypeInvalidTest((nullish, type) => ({ code: ` @@ -494,7 +710,7 @@ a || b || c; output: null, errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishOverOr', line: 5, column: 3, endLine: 5, diff --git a/packages/eslint-plugin/tests/util/isNodeEqual.test.ts b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts new file mode 100644 index 000000000000..76da9eabdb42 --- /dev/null +++ b/packages/eslint-plugin/tests/util/isNodeEqual.test.ts @@ -0,0 +1,106 @@ +import { TSESTree, TSESLint } from '@typescript-eslint/utils'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; +import { createRule, isNodeEqual } from '../../src/util'; + +const rule = createRule({ + name: 'no-useless-expression', + defaultOptions: [], + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Remove useless expressions.', + recommended: false, + }, + messages: { + removeExpression: 'Remove useless expression', + }, + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + LogicalExpression: (node: TSESTree.LogicalExpression): void => { + if ( + (node.operator === '??' || + node.operator === '||' || + node.operator === '&&') && + isNodeEqual(node.left, node.right) + ) { + context.report({ + node, + messageId: 'removeExpression', + fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + return fixer.replaceText( + node, + sourceCode.text.slice(node.left.range[0], node.left.range[1]), + ); + }, + }); + } + }, + }; + }, +}); + +const rootPath = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('isNodeEqual', rule, { + valid: [ + { code: 'a || b' }, + { code: '!true || true' }, + { code: 'a() || a()' }, + { code: 'foo.bar || foo.bar.foo' }, + ], + invalid: [ + { + code: 'undefined || undefined', + errors: [{ messageId: 'removeExpression' }], + output: 'undefined', + }, + { + code: 'true && true', + errors: [{ messageId: 'removeExpression' }], + output: 'true', + }, + { + code: 'a || a', + errors: [{ messageId: 'removeExpression' }], + output: 'a', + }, + { + code: 'a && a', + errors: [{ messageId: 'removeExpression' }], + output: 'a', + }, + { + code: 'a ?? a', + errors: [{ messageId: 'removeExpression' }], + output: 'a', + }, + { + code: 'foo.bar || foo.bar', + errors: [{ messageId: 'removeExpression' }], + output: 'foo.bar', + }, + { + code: 'this.foo.bar || this.foo.bar', + errors: [{ messageId: 'removeExpression' }], + output: 'this.foo.bar', + }, + { + code: 'x.z[1][this[this.o]]["3"][a.b.c] || x.z[1][this[this.o]]["3"][a.b.c]', + errors: [{ messageId: 'removeExpression' }], + output: 'x.z[1][this[this.o]]["3"][a.b.c]', + }, + ], +}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy