From 4bdbe67955fd591c25e58b13e674ba05bf5ed585 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 16 Nov 2022 01:48:06 -0500 Subject: [PATCH] feat(eslint-plugin): [prefer-nullish-coalescing]: add support for assignment expressions (#5234) BREAKING CHANGE: Adds an additional class of checks to the rule --- .../docs/rules/prefer-nullish-coalescing.md | 15 +- .../src/rules/prefer-nullish-coalescing.ts | 149 +++++---- .../rules/prefer-nullish-coalescing.test.ts | 298 +++++++++--------- 3 files changed, 254 insertions(+), 208 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index d2fc95f4066..5aa780f8002 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -1,5 +1,5 @@ --- -description: 'Enforce using the nullish coalescing operator instead of logical chaining.' +description: 'Enforce using the nullish coalescing operator instead of logical assignments or chaining.' --- > 🛑 This file is source code, not the primary documentation location! 🛑 @@ -9,7 +9,10 @@ description: 'Enforce using the nullish coalescing operator instead of logical c The `??` nullish coalescing runtime operator allows providing a default value when dealing with `null` or `undefined`. Because the nullish coalescing operator _only_ coalesces when the original value is `null` or `undefined`, it is much safer than relying upon logical OR operator chaining `||`, which coalesces on any _falsy_ value. -This rule reports when an `||` operator can be safely replaced with a `??`. +This rule reports when you can safely replace: + +- An `||` operator with `??` +- An `||=` operator with `??=` :::caution This rule will not work as expected if [`strictNullChecks`](https://www.typescriptlang.org/tsconfig#strictNullChecks) is not enabled. @@ -73,7 +76,10 @@ declare const b: string | null; if (a || b) { } +if ((a ||= b)) { +} while (a || b) {} +while ((a ||= b)) {} do {} while (a || b); for (let i = 0; a || b; i += 1) {} a || b ? true : false; @@ -87,7 +93,10 @@ declare const b: string | null; if (a ?? b) { } +if ((a ??= b)) { +} while (a ?? b) {} +while ((a ??= b)) {} do {} while (a ?? b); for (let i = 0; a ?? b; i += 1) {} a ?? b ? true : false; @@ -110,6 +119,7 @@ declare const c: string | null; declare const d: string | null; a || (b && c); +a ||= b && c; (a && b) || c || d; a || (b && c) || d; a || (b && c && d); @@ -124,6 +134,7 @@ declare const c: string | null; declare const d: string | null; a ?? (b && c); +a ??= b && c; (a && b) ?? c ?? d; a ?? (b && c) ?? d; a ?? (b && c && d); diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index ca40160e982..f29df6f6f98 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -23,17 +23,17 @@ export default util.createRule({ type: 'suggestion', docs: { description: - 'Enforce using the nullish coalescing operator instead of logical chaining', + 'Enforce using the nullish coalescing operator instead of logical assignments or chaining', recommended: 'strict', requiresTypeChecking: true, }, hasSuggestions: true, messages: { preferNullishOverOr: - 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', + 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a logical {{ description }} (`||{{ equals }}`), 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 (`??`).', + 'Prefer using nullish coalescing operator (`??{{ equals }}`) instead of a ternary expression, as it is simpler to read.', + suggestNullish: 'Fix to nullish coalescing operator (`??{{ equals }}`).', }, schema: [ { @@ -74,6 +74,75 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const checker = parserServices.program.getTypeChecker(); + // todo: rename to something more specific? + function checkAssignmentOrLogicalExpression( + node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, + description: string, + equals: string, + ): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode.left); + const isNullish = util.isNullableType(type, { allowUndefined: true }); + if (!isNullish) { + return; + } + + if (ignoreConditionalTests === true && isConditionalTest(node)) { + return; + } + + if ( + ignoreMixedLogicalExpressions === true && + isMixedLogicalExpression(node) + ) { + return; + } + + const barBarOperator = util.nullThrows( + sourceCode.getTokenAfter( + node.left, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === node.operator, + ), + util.NullThrowsReasons.MissingToken('operator', node.type), + ); + + function* fix( + fixer: TSESLint.RuleFixer, + ): IterableIterator { + if (node.parent && util.isLogicalOrOperator(node.parent)) { + // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) + if ( + node.left.type === AST_NODE_TYPES.LogicalExpression && + !util.isLogicalOrOperator(node.left.left) + ) { + yield fixer.insertTextBefore(node.left.right, '('); + } else { + yield fixer.insertTextBefore(node.left, '('); + } + yield fixer.insertTextAfter(node.right, ')'); + } + yield fixer.replaceText( + barBarOperator, + node.operator.replace('||', '??'), + ); + } + + context.report({ + data: { equals, description }, + node: barBarOperator, + messageId: 'preferNullishOverOr', + suggest: [ + { + data: { equals }, + messageId: 'suggestNullish', + fix, + }, + ], + }); + } + return { ConditionalExpression(node: TSESTree.ConditionalExpression): void { if (ignoreTernaryTests) { @@ -103,7 +172,7 @@ export default util.createRule({ node.test.right.left, node.test.right.right, ]; - if (node.test.operator === '||') { + if (['||', '||='].includes(node.test.operator)) { if ( node.test.left.operator === '===' && node.test.right.operator === '===' @@ -205,10 +274,13 @@ export default util.createRule({ if (isFixable) { context.report({ + // TODO: also account for = in the ternary clause + data: { equals: '' }, node, messageId: 'preferNullishOverTernary', suggest: [ { + data: { equals: '' }, messageId: 'suggestNullish', fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = @@ -231,64 +303,15 @@ export default util.createRule({ }); } }, - + 'AssignmentExpression[operator = "||="]'( + node: TSESTree.AssignmentExpression, + ): void { + checkAssignmentOrLogicalExpression(node, 'assignment', '='); + }, 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode.left); - const isNullish = util.isNullableType(type, { allowUndefined: true }); - if (!isNullish) { - return; - } - - if (ignoreConditionalTests === true && isConditionalTest(node)) { - return; - } - - const isMixedLogical = isMixedLogicalExpression(node); - if (ignoreMixedLogicalExpressions === true && isMixedLogical) { - return; - } - - const barBarOperator = util.nullThrows( - sourceCode.getTokenAfter( - node.left, - token => - token.type === AST_TOKEN_TYPES.Punctuator && - token.value === node.operator, - ), - util.NullThrowsReasons.MissingToken('operator', node.type), - ); - - function* fix( - fixer: TSESLint.RuleFixer, - ): IterableIterator { - if (node.parent && util.isLogicalOrOperator(node.parent)) { - // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) - if ( - node.left.type === AST_NODE_TYPES.LogicalExpression && - !util.isLogicalOrOperator(node.left.left) - ) { - yield fixer.insertTextBefore(node.left.right, '('); - } else { - yield fixer.insertTextBefore(node.left, '('); - } - yield fixer.insertTextAfter(node.right, ')'); - } - yield fixer.replaceText(barBarOperator, '??'); - } - - context.report({ - node: barBarOperator, - messageId: 'preferNullishOverOr', - suggest: [ - { - messageId: 'suggestNullish', - fix, - }, - ], - }); + checkAssignmentOrLogicalExpression(node, 'or', ''); }, }; }, @@ -331,7 +354,9 @@ function isConditionalTest(node: TSESTree.Node): boolean { return false; } -function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean { +function isMixedLogicalExpression( + node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, +): boolean { const seen = new Set(); const queue = [node.parent, node.left, node.right]; for (const current of queue) { @@ -343,7 +368,7 @@ function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean { if (current && current.type === AST_NODE_TYPES.LogicalExpression) { if (current.operator === '&&') { return true; - } else if (current.operator === '||') { + } else if (['||', '||='].includes(current.operator)) { // check the pieces of the node to catch cases like `a || b || c && d` queue.push(current.parent, current.left, current.right); } 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 3824f464a58..62a7cece074 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -21,20 +21,28 @@ const types = ['string', 'number', 'boolean', 'object']; const nullishTypes = ['null', 'undefined', 'null | undefined']; function typeValidTest( - cb: (type: string) => TSESLint.ValidTestCase | string, + cb: ( + type: string, + equals: '' | '=', + ) => TSESLint.ValidTestCase | string, ): (TSESLint.ValidTestCase | string)[] { - return types.map(type => cb(type)); + return [ + ...types.map(type => cb(type, '')), + ...types.map(type => cb(type, '=')), + ]; } + function nullishTypeValidTest( cb: ( nullish: string, type: string, + equals: string, ) => TSESLint.ValidTestCase | string, ): (TSESLint.ValidTestCase | string)[] { return nullishTypes.reduce<(TSESLint.ValidTestCase | string)[]>( (acc, nullish) => { types.forEach(type => { - acc.push(cb(nullish, type)); + acc.push(cb(nullish, type, ''), cb(nullish, type, '=')); }); return acc; }, @@ -45,12 +53,14 @@ function nullishTypeInvalidTest( cb: ( nullish: string, type: string, + equals: string, ) => TSESLint.InvalidTestCase, ): TSESLint.InvalidTestCase[] { return nullishTypes.reduce[]>( (acc, nullish) => { types.forEach(type => { - acc.push(cb(nullish, type)); + acc.push(cb(nullish, type, '')); + acc.push(cb(nullish, type, '=')); }); return acc; }, @@ -61,15 +71,15 @@ function nullishTypeInvalidTest( ruleTester.run('prefer-nullish-coalescing', rule, { valid: [ ...typeValidTest( - type => ` -declare const x: ${type}; -x || 'foo'; + (type, equals) => ` +declare let x: ${type}; +(x ||${equals} 'foo'); `, ), ...nullishTypeValidTest( - (nullish, type) => ` -declare const x: ${type} | ${nullish}; -x ?? 'foo'; + (nullish, type, equals) => ` +declare let x: ${type} | ${nullish}; +x ??${equals} 'foo'; `, ), @@ -102,35 +112,35 @@ x ?? 'foo'; 'null != x ? y : x;', 'undefined != x ? y : x;', ` -declare const x: string; +declare let x: string; x === null ? x : y; `, ` -declare const x: string | undefined; +declare let x: string | undefined; x === null ? x : y; `, ` -declare const x: string | null; +declare let x: string | null; x === undefined ? x : y; `, ` -declare const x: string | undefined | null; +declare let x: string | undefined | null; x !== undefined ? x : y; `, ` -declare const x: string | undefined | null; +declare let x: string | undefined | null; x !== null ? x : y; `, ` -declare const x: string | null | any; +declare let x: string | null | any; x === null ? x : y; `, ` -declare const x: string | null | unknown; +declare let x: string | null | unknown; x === null ? x : y; `, ` -declare const x: string | undefined; +declare let x: string | undefined; x === null ? x : y; `, ].map(code => ({ @@ -139,38 +149,38 @@ x === null ? x : y; })), // ignoreConditionalTests - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (x || 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -do {} while (x || 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -for (;x || 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -while (x || 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} `, options: [{ ignoreConditionalTests: true }], })), @@ -178,54 +188,54 @@ while (x || 'foo') {} // ignoreMixedLogicalExpressions ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; a || b && c; `, options: [{ ignoreMixedLogicalExpressions: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a || b || c && d; `, options: [{ ignoreMixedLogicalExpressions: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a && b || c || d; `, options: [{ ignoreMixedLogicalExpressions: true }], })), ], invalid: [ - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo'; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo'); `, output: null, errors: [ { messageId: 'preferNullishOverOr', line: 3, - column: 3, + column: 4, endLine: 3, - endColumn: 5, + endColumn: 6 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo'; +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo'); `, }, ], @@ -330,35 +340,35 @@ x ?? 'foo'; ...[ ` -declare const x: string | undefined; +declare let x: string | undefined; x !== undefined ? x : y; `, ` -declare const x: string | undefined; +declare let x: string | undefined; undefined !== x ? x : y; `, ` -declare const x: string | undefined; +declare let x: string | undefined; undefined === x ? y : x; `, ` -declare const x: string | undefined; +declare let x: string | undefined; undefined === x ? y : x; `, ` -declare const x: string | null; +declare let x: string | null; x !== null ? x : y; `, ` -declare const x: string | null; +declare let x: string | null; null !== x ? x : y; `, ` -declare const x: string | null; +declare let x: string | null; null === x ? y : x; `, ` -declare const x: string | null; +declare let x: string | null; null === x ? y : x; `, ].map(code => ({ @@ -386,10 +396,10 @@ x ?? y; })), // ignoreConditionalTests - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; `, output: null, options: [{ ignoreConditionalTests: false }], @@ -397,25 +407,25 @@ x || 'foo' ? null : null; { messageId: 'preferNullishOverOr', line: 3, - column: 3, + column: 4, endLine: 3, - endColumn: 5, + endColumn: 6 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo') ? null : null; `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (x || 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -423,25 +433,25 @@ if (x || 'foo') {} { messageId: 'preferNullishOverOr', line: 3, - column: 7, + column: 8, endLine: 3, - endColumn: 9, + endColumn: 10 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -if (x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ??${equals} 'foo')) {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -do {} while (x || 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) `, output: null, options: [{ ignoreConditionalTests: false }], @@ -449,25 +459,25 @@ do {} while (x || 'foo') { messageId: 'preferNullishOverOr', line: 3, - column: 16, + column: 17, endLine: 3, - endColumn: 18, + endColumn: 19 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -do {} while (x ?? 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ??${equals} 'foo')) `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -for (;x || 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -475,25 +485,25 @@ for (;x || 'foo';) {} { messageId: 'preferNullishOverOr', line: 3, - column: 9, + column: 10, endLine: 3, - endColumn: 11, + endColumn: 12 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -for (;x ?? 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ??${equals} 'foo');) {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -while (x || 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -501,15 +511,15 @@ while (x || 'foo') {} { messageId: 'preferNullishOverOr', line: 3, - column: 10, + column: 11, endLine: 3, - endColumn: 12, + endColumn: 13 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -while (x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ??${equals} 'foo')) {} `, }, ], @@ -520,9 +530,9 @@ while (x ?? 'foo') {} // ignoreMixedLogicalExpressions ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; a || b && c; `, options: [{ ignoreMixedLogicalExpressions: false }], @@ -537,9 +547,9 @@ a || b && c; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; a ?? b && c; `, }, @@ -549,10 +559,10 @@ a ?? b && c; })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a || b || c && d; `, options: [{ ignoreMixedLogicalExpressions: false }], @@ -567,10 +577,10 @@ a || b || c && d; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; (a ?? b) || c && d; `, }, @@ -586,10 +596,10 @@ declare const d: ${type} | ${nullish}; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a || b ?? c && d; `, }, @@ -599,10 +609,10 @@ a || b ?? c && d; })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a && b || c || d; `, options: [{ ignoreMixedLogicalExpressions: false }], @@ -617,10 +627,10 @@ a && b || c || d; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a && (b ?? c) || d; `, }, @@ -636,10 +646,10 @@ a && (b ?? c) || d; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type} | ${nullish}; -declare const c: ${type} | ${nullish}; -declare const d: ${type} | ${nullish}; +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a && b || c ?? d; `, }, @@ -649,10 +659,10 @@ a && b || c ?? d; })), // should not false positive for functions inside conditional tests - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (() => x || 'foo') {} +declare let x: ${type} | ${nullish}; +if (() => (x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: true }], @@ -660,25 +670,25 @@ if (() => x || 'foo') {} { messageId: 'preferNullishOverOr', line: 3, - column: 13, + column: 14, endLine: 3, - endColumn: 15, + endColumn: 16 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -if (() => x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +if (() => (x ??${equals} 'foo')) {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (function werid() { return x || 'foo' }) {} +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ||${equals} 'foo') }) {} `, output: null, options: [{ ignoreConditionalTests: true }], @@ -686,15 +696,15 @@ if (function werid() { return x || 'foo' }) {} { messageId: 'preferNullishOverOr', line: 3, - column: 33, + column: 34, endLine: 3, - endColumn: 35, + endColumn: 36 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -if (function werid() { return x ?? 'foo' }) {} +declare let x: ${type} | ${nullish}; +if (function weird() { return (x ??${equals} 'foo') }) {} `, }, ], @@ -704,9 +714,9 @@ if (function werid() { return x ?? 'foo' }) {} // https://github.com/typescript-eslint/typescript-eslint/issues/1290 ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type}; -declare const c: ${type}; +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; a || b || c; `, output: null, @@ -721,9 +731,9 @@ a || b || c; { messageId: 'suggestNullish', output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type}; -declare const c: ${type}; +declare let a: ${type} | ${nullish}; +declare let b: ${type}; +declare let c: ${type}; (a ?? b) || c; `, },