From dcb43d0bc31788240e9afeac4276fb92707b61cd Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 23 Jun 2022 11:43:45 -0400 Subject: [PATCH 1/5] feat(eslint-plugin) [prefer-nullish-coalescing]: add support for assignment expressions --- .../docs/rules/prefer-nullish-coalescing.md | 16 +- .../src/rules/prefer-nullish-coalescing.ts | 146 +++--- .../rules/prefer-nullish-coalescing.test.ts | 482 +++++++++++++----- 3 files changed, 453 insertions(+), 191 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index 9b1dfe988209..8c36b2a90ba5 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -1,9 +1,11 @@ # `prefer-nullish-coalescing` -Enforces using the nullish coalescing operator instead of logical chaining. +Enforces using the nullish coalescing operator instead of logical assignments or chaining. -TypeScript 3.7 added support for the nullish coalescing operator. -This operator allows you to safely cascade a value when dealing with `null` or `undefined`. +This operator allows you to safely cascade a value when dealing with `null` or `undefined` in: + +- Assignment expressions: `left ||= right` +- Logical expressions: `left || right` ```ts function myFunc(foo: string | null) { @@ -75,7 +77,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; @@ -89,7 +94,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; @@ -112,6 +120,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); @@ -126,6 +135,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 c1676d3fd323..e617c2963c96 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -12,7 +12,11 @@ export type Options = [ ignoreMixedLogicalExpressions?: boolean; }, ]; -export type MessageIds = 'preferNullish' | 'suggestNullish'; +export type MessageIds = + | 'preferNullishAssignment' + | 'preferNullishLogical' + | 'suggestNullishAssignment' + | 'suggestNullishLogical'; export default util.createRule({ name: 'prefer-nullish-coalescing', @@ -27,9 +31,12 @@ export default util.createRule({ }, hasSuggestions: true, messages: { - preferNullish: + preferNullishAssignment: + 'Prefer using nullish coalescing operator (`??=`) instead of a logical assignment (`||=`), as it is a safer operator.', + preferNullishLogical: 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', - suggestNullish: 'Fix to nullish coalescing operator (`??`).', + suggestNullishAssignment: 'Fix to nullish coalescing assignment (`??=`).', + suggestNullishLogical: 'Fix to nullish coalescing operator (`??`).', }, schema: [ { @@ -41,9 +48,6 @@ export default util.createRule({ ignoreMixedLogicalExpressions: { type: 'boolean', }, - forceSuggestionFixer: { - type: 'boolean', - }, }, additionalProperties: false, }, @@ -60,70 +64,90 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const checker = parserServices.program.getTypeChecker(); - return { - '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; - } + function checkNode( + node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, + reportMessageId: MessageIds, + suggestionMessageId: MessageIds, + ): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode.left); + const isNullish = util.isNullableType(type, { allowUndefined: true }); + if (!isNullish) { + return; + } - const isMixedLogical = isMixedLogicalExpression(node); - if (ignoreMixedLogicalExpressions === true && isMixedLogical) { - return; - } + if (ignoreConditionalTests === true && isConditionalTest(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), - ); + if ( + ignoreMixedLogicalExpressions === true && + isMixedLogicalExpression(node) + ) { + return; + } - 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, ')'); + 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.replaceText(barBarOperator, '??'); + yield fixer.insertTextAfter(node.right, ')'); } + yield fixer.replaceText( + barBarOperator, + node.operator.replace('||', '??'), + ); + } + + context.report({ + node: barBarOperator, + messageId: reportMessageId, // 'preferNullish', + suggest: [ + { + messageId: suggestionMessageId, // 'suggestNullish', + fix, + }, + ], + }); + } - context.report({ - node: barBarOperator, - messageId: 'preferNullish', - suggest: [ - { - messageId: 'suggestNullish', - fix, - }, - ], - }); + return { + 'AssignmentExpression[operator = "||="]'( + node: TSESTree.AssignmentExpression, + ): void { + checkNode(node, 'preferNullishAssignment', 'suggestNullishAssignment'); + }, + 'LogicalExpression[operator = "||"]'( + node: TSESTree.LogicalExpression, + ): void { + checkNode(node, 'preferNullishLogical', 'suggestNullishLogical'); }, }; }, }); -function isConditionalTest(node: TSESTree.Node): boolean { +function isConditionalTest( + node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression, +): boolean { const parents = new Set([node]); let current = node.parent; while (current) { @@ -160,7 +184,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) { 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..76c15ceaa4f2 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -19,9 +19,15 @@ 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: ( @@ -59,50 +65,70 @@ 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}; +declare let x: ${type} | ${nullish}; x ?? 'foo'; `, ), + ...nullishTypeValidTest( + (nullish, type) => ` +declare let x: ${type} | ${nullish}; +x ??= 'foo'; + `, + ), // ignoreConditionalTests ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; x || 'foo' ? null : null; `, options: [{ ignoreConditionalTests: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; +(x ||= 'foo') ? null : null; + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; if (x || 'foo') {} `, options: [{ ignoreConditionalTests: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; +if (x ||= 'foo') {} + `, + options: [{ ignoreConditionalTests: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; do {} while (x || 'foo') `, options: [{ ignoreConditionalTests: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; for (;x || 'foo';) {} `, options: [{ ignoreConditionalTests: true }], })), ...nullishTypeValidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; while (x || 'foo') {} `, options: [{ ignoreConditionalTests: true }], @@ -111,29 +137,48 @@ 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}; +a ||= b && c; + `, + options: [{ ignoreMixedLogicalExpressions: true }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +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 }], + })), + ...nullishTypeValidTest((nullish, type) => ({ + code: ` +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 }], @@ -142,24 +187,74 @@ a && b || c || d; invalid: [ ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; x || 'foo'; - `.trimRight(), + `.trimEnd(), output: null, errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 3, endLine: 3, endColumn: 5, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; x ?? 'foo'; - `.trimRight(), + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; +x ||= 'foo'; + `.trimEnd(), + output: null, + errors: [ + { + messageId: 'preferNullishAssignment', + line: 3, + column: 3, + endLine: 3, + endColumn: 6, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let x: ${type} | ${nullish}; +x ??= 'foo'; + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; +x ||= 'foo' ? null : null; + `.trimEnd(), + output: null, + errors: [ + { + messageId: 'preferNullishAssignment', + line: 3, + column: 3, + endLine: 3, + endColumn: 6, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let x: ${type} | ${nullish}; +x ??= 'foo' ? null : null; + `.trimEnd(), }, ], }, @@ -169,25 +264,25 @@ x ?? 'foo'; // ignoreConditionalTests ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; x || 'foo' ? null : null; - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 3, endLine: 3, endColumn: 5, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; x ?? 'foo' ? null : null; - `.trimRight(), + `.trimEnd(), }, ], }, @@ -195,25 +290,51 @@ x ?? 'foo' ? null : null; })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (x || 'foo') {} - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 7, endLine: 3, endColumn: 9, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (x ?? 'foo') {} - `.trimRight(), + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; +if (x ||= 'foo') {} + `.trimEnd(), + output: null, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullishAssignment', + line: 3, + column: 7, + endLine: 3, + endColumn: 10, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let x: ${type} | ${nullish}; +if (x ??= 'foo') {} + `.trimEnd(), }, ], }, @@ -221,25 +342,25 @@ if (x ?? 'foo') {} })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; do {} while (x || 'foo') - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 16, endLine: 3, endColumn: 18, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; do {} while (x ?? 'foo') - `.trimRight(), + `.trimEnd(), }, ], }, @@ -247,25 +368,51 @@ do {} while (x ?? 'foo') })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; +do {} while (x ||= 'foo') + `.trimEnd(), + output: null, + options: [{ ignoreConditionalTests: false }], + errors: [ + { + messageId: 'preferNullishAssignment', + line: 3, + column: 16, + endLine: 3, + endColumn: 19, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let x: ${type} | ${nullish}; +do {} while (x ??= 'foo') + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let x: ${type} | ${nullish}; for (;x || 'foo';) {} - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 9, endLine: 3, endColumn: 11, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; for (;x ?? 'foo';) {} - `.trimRight(), + `.trimEnd(), }, ], }, @@ -273,25 +420,25 @@ for (;x ?? 'foo';) {} })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; while (x || 'foo') {} - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 10, endLine: 3, endColumn: 12, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; while (x ?? 'foo') {} - `.trimRight(), + `.trimEnd(), }, ], }, @@ -301,28 +448,28 @@ 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; - `.trimRight(), + `.trimEnd(), options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 5, column: 3, endLine: 5, endColumn: 5, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), }, ], }, @@ -330,49 +477,128 @@ 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}; +a ||= b && c; + `.trimEnd(), + options: [{ ignoreMixedLogicalExpressions: false }], + errors: [ + { + messageId: 'preferNullishAssignment', + line: 5, + column: 3, + endLine: 5, + endColumn: 6, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +a ??= b && c; + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; a || b || c && d; - `.trimRight(), + `.trimEnd(), options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 6, column: 3, endLine: 6, endColumn: 5, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), }, ], }, { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 6, column: 8, endLine: 6, endColumn: 10, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), + }, + ], + }, + ], + })), + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a ||= b || c && d; + `.trimEnd(), + options: [{ ignoreMixedLogicalExpressions: false }], + errors: [ + { + messageId: 'preferNullishAssignment', + line: 6, + column: 3, + endLine: 6, + endColumn: 6, + suggestions: [ + { + messageId: 'suggestNullishAssignment', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a ??= b || c && d; + `.trimEnd(), + }, + ], + }, + { + messageId: 'preferNullishLogical', + line: 6, + column: 9, + endLine: 6, + endColumn: 11, + suggestions: [ + { + messageId: 'suggestNullishLogical', + output: ` +declare let a: ${type} | ${nullish}; +declare let b: ${type} | ${nullish}; +declare let c: ${type} | ${nullish}; +declare let d: ${type} | ${nullish}; +a ||= b ?? c && d; + `.trimEnd(), }, ], }, @@ -380,49 +606,49 @@ 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; - `.trimRight(), + `.trimEnd(), options: [{ ignoreMixedLogicalExpressions: false }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 6, column: 8, endLine: 6, endColumn: 10, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), }, ], }, { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 6, column: 13, endLine: 6, endColumn: 15, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), }, ], }, @@ -432,25 +658,25 @@ a && b || c ?? d; // should not false positive for functions inside conditional tests ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (() => x || 'foo') {} - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: true }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 13, endLine: 3, endColumn: 15, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (() => x ?? 'foo') {} - `.trimRight(), + `.trimEnd(), }, ], }, @@ -458,25 +684,25 @@ if (() => x ?? 'foo') {} })), ...nullishTypeInvalidTest((nullish, type) => ({ code: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (function werid() { return x || 'foo' }) {} - `.trimRight(), + `.trimEnd(), output: null, options: [{ ignoreConditionalTests: true }], errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 3, column: 33, endLine: 3, endColumn: 35, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', output: ` -declare const x: ${type} | ${nullish}; +declare let x: ${type} | ${nullish}; if (function werid() { return x ?? 'foo' }) {} - `.trimRight(), + `.trimEnd(), }, ], }, @@ -486,28 +712,28 @@ 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; - `.trimRight(), + `.trimEnd(), output: null, errors: [ { - messageId: 'preferNullish', + messageId: 'preferNullishLogical', line: 5, column: 3, endLine: 5, endColumn: 5, suggestions: [ { - messageId: 'suggestNullish', + messageId: 'suggestNullishLogical', 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; - `.trimRight(), + `.trimEnd(), }, ], }, From 5603522f28eee336a4b9a5ddabb79ce356a53517 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 23 Jun 2022 12:01:23 -0400 Subject: [PATCH 2/5] fix: updated all rule docs --- packages/eslint-plugin/README.md | 2 +- packages/eslint-plugin/docs/rules/README.md | 2 +- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 60a769945183..3d9522b90b57 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -161,7 +161,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-literal-enum-member`](./docs/rules/prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | | [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | +| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical assignments or chaining | :lock: | | :thought_balloon: | | [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands | :lock: | | | | [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/README.md b/packages/eslint-plugin/docs/rules/README.md index 805f126f8228..7d4443d2721e 100644 --- a/packages/eslint-plugin/docs/rules/README.md +++ b/packages/eslint-plugin/docs/rules/README.md @@ -83,7 +83,7 @@ See [Configs](/docs/linting/configs) for how to enable recommended rules using c | [`@typescript-eslint/prefer-includes`](./prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-literal-enum-member`](./prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | | [`@typescript-eslint/prefer-namespace-keyword`](./prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | +| [`@typescript-eslint/prefer-nullish-coalescing`](./prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical assignments or chaining | :lock: | | :thought_balloon: | | [`@typescript-eslint/prefer-optional-chain`](./prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands | :lock: | | | | [`@typescript-eslint/prefer-readonly`](./prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-readonly-parameter-types`](./prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index e617c2963c96..3c279d2ac801 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -24,7 +24,7 @@ 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', suggestion: true, requiresTypeChecking: true, From 4b965a49bd4feb1b3903fbb4a5eefa2fdd6905f6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 25 Oct 2022 19:57:53 -0400 Subject: [PATCH 3/5] WIP: progress on tests --- .../rules/prefer-nullish-coalescing.test.ts | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) 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 e1bfc41a68a8..62630bc147a6 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -53,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; }, @@ -71,7 +73,7 @@ ruleTester.run('prefer-nullish-coalescing', rule, { ...typeValidTest( (type, equals) => ` declare let x: ${type}; -x ||${equals} 'foo'; +(x ||${equals} 'foo'); `, ), ...nullishTypeValidTest( @@ -150,35 +152,35 @@ x === null ? x : y; ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -x ||${equals} 'foo' ? null : null; +(x ||${equals} 'foo') ? null : null; `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -if (x || 'foo') {} +if ((x ||${equals} 'foo')) {} `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -do {} while (x || 'foo') +do {} while ((x ||${equals} 'foo')) `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -for (;x || 'foo';) {} +for (;(x ||${equals} 'foo';)) {} `, options: [{ ignoreConditionalTests: true }], })), - ...nullishTypeValidTest((nullish, type) => ({ + ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -while (x || 'foo') {} +while ((x ||${equals} 'foo')) {} `, options: [{ ignoreConditionalTests: true }], })), @@ -215,10 +217,10 @@ a && b || c || d; })), ], invalid: [ - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -x || 'foo'; +(x ||${equals} 'foo'); `, output: null, errors: [ @@ -233,7 +235,7 @@ x || 'foo'; messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -x ?? 'foo'; +(x ??${equals} 'foo'); `, }, ], @@ -394,10 +396,10 @@ x ?? y; })), // ignoreConditionalTests - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -x || 'foo' ? null : null; +(x ||${equals} 'foo') ? null : null; `, output: null, options: [{ ignoreConditionalTests: false }], @@ -413,17 +415,17 @@ x || 'foo' ? null : null; messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -x ?? 'foo' ? null : null; +(x ??${equals} 'foo') ? null : null; `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -if (x || 'foo') {} +if ((x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -439,17 +441,17 @@ if (x || 'foo') {} messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -if (x ?? 'foo') {} +if (x ??${equals} 'foo') {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -do {} while (x || 'foo') +do {} while ((x ||${equals} 'foo')) `, output: null, options: [{ ignoreConditionalTests: false }], @@ -465,17 +467,17 @@ do {} while (x || 'foo') messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -do {} while (x ?? 'foo') +do {} while (x ??${equals} 'foo') `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -for (;x || 'foo';) {} +for (;(x ||${equals} 'foo';)) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -491,17 +493,17 @@ for (;x || 'foo';) {} messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -for (;x ?? 'foo';) {} +for (;(x ??${equals} 'foo');) {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -while (x || 'foo') {} +while ((x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -517,7 +519,7 @@ while (x || 'foo') {} messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -while (x ?? 'foo') {} +while (x ??${equals} 'foo') {} `, }, ], @@ -657,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 let x: ${type} | ${nullish}; -if (() => x || 'foo') {} +if (() => (x ||${equals} 'foo')) {} `, output: null, options: [{ ignoreConditionalTests: true }], @@ -668,25 +670,25 @@ if (() => x || 'foo') {} { messageId: 'preferNullishOverOr', line: 3, - column: 13, + column: 14 + equals.length, endLine: 3, - endColumn: 15, + endColumn: 15 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -if (() => x ?? 'foo') {} +if (() => (x ??${equals} 'foo')) {} `, }, ], }, ], })), - ...nullishTypeInvalidTest((nullish, type) => ({ + ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -if (function werid() { return x || 'foo' }) {} +if (function weird() { return (x ||${equals} 'foo') }) {} `, output: null, options: [{ ignoreConditionalTests: true }], @@ -694,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 let x: ${type} | ${nullish}; -if (function werid() { return x ?? 'foo' }) {} +if (function weird() { return (x ??${equals} 'foo') }) {} `, }, ], From ce35f2402e921ef88f7ceda439bdac36afe17779 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 26 Oct 2022 10:43:17 -0400 Subject: [PATCH 4/5] Finish fixing tests --- .../rules/prefer-nullish-coalescing.test.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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 62630bc147a6..62a7cece074a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -173,7 +173,7 @@ do {} while ((x ||${equals} 'foo')) ...nullishTypeValidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -for (;(x ||${equals} 'foo';)) {} +for (;(x ||${equals} 'foo');) {} `, options: [{ ignoreConditionalTests: true }], })), @@ -227,9 +227,9 @@ declare let x: ${type} | ${nullish}; { messageId: 'preferNullishOverOr', line: 3, - column: 3, + column: 4, endLine: 3, - endColumn: 5, + endColumn: 6 + equals.length, suggestions: [ { messageId: 'suggestNullish', @@ -407,9 +407,9 @@ declare let x: ${type} | ${nullish}; { messageId: 'preferNullishOverOr', line: 3, - column: 3, + column: 4, endLine: 3, - endColumn: 5, + endColumn: 6 + equals.length, suggestions: [ { messageId: 'suggestNullish', @@ -433,15 +433,15 @@ if ((x ||${equals} 'foo')) {} { messageId: 'preferNullishOverOr', line: 3, - column: 7, + column: 8, endLine: 3, - endColumn: 9, + endColumn: 10 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -if (x ??${equals} 'foo') {} +if ((x ??${equals} 'foo')) {} `, }, ], @@ -459,15 +459,15 @@ do {} while ((x ||${equals} 'foo')) { messageId: 'preferNullishOverOr', line: 3, - column: 16, + column: 17, endLine: 3, - endColumn: 18, + endColumn: 19 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -do {} while (x ??${equals} 'foo') +do {} while ((x ??${equals} 'foo')) `, }, ], @@ -477,7 +477,7 @@ do {} while (x ??${equals} 'foo') ...nullishTypeInvalidTest((nullish, type, equals) => ({ code: ` declare let x: ${type} | ${nullish}; -for (;(x ||${equals} 'foo';)) {} +for (;(x ||${equals} 'foo');) {} `, output: null, options: [{ ignoreConditionalTests: false }], @@ -485,9 +485,9 @@ for (;(x ||${equals} 'foo';)) {} { messageId: 'preferNullishOverOr', line: 3, - column: 9, + column: 10, endLine: 3, - endColumn: 11, + endColumn: 12 + equals.length, suggestions: [ { messageId: 'suggestNullish', @@ -511,15 +511,15 @@ while ((x ||${equals} 'foo')) {} { messageId: 'preferNullishOverOr', line: 3, - column: 10, + column: 11, endLine: 3, - endColumn: 12, + endColumn: 13 + equals.length, suggestions: [ { messageId: 'suggestNullish', output: ` declare let x: ${type} | ${nullish}; -while (x ??${equals} 'foo') {} +while ((x ??${equals} 'foo')) {} `, }, ], @@ -670,9 +670,9 @@ if (() => (x ||${equals} 'foo')) {} { messageId: 'preferNullishOverOr', line: 3, - column: 14 + equals.length, + column: 14, endLine: 3, - endColumn: 15 + equals.length, + endColumn: 16 + equals.length, suggestions: [ { messageId: 'suggestNullish', From 9678f6c86c74d3ddd236f000f61b95745ad35c54 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 26 Oct 2022 11:00:29 -0400 Subject: [PATCH 5/5] Fix rule description --- packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 83d3ed179ac1..f29df6f6f98d 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -23,7 +23,7 @@ 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, },