diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 56e77925b489..8e3f3db8e87f 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -56,10 +56,10 @@ export default createRule({ noStrictNullCheck: 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', 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: [ { @@ -171,7 +171,107 @@ export default createRule({ }); } + // 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); + if (!isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { + return; + } + + if (ignoreConditionalTests === true && isConditionalTest(node)) { + return; + } + + if ( + ignoreMixedLogicalExpressions === true && + isMixedLogicalExpression(node) + ) { + return; + } + + // https://github.com/typescript-eslint/typescript-eslint/issues/5439 + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + const ignorableFlags = [ + (ignorePrimitives === true || ignorePrimitives!.bigint) && + ts.TypeFlags.BigIntLike, + (ignorePrimitives === true || ignorePrimitives!.boolean) && + ts.TypeFlags.BooleanLike, + (ignorePrimitives === true || ignorePrimitives!.number) && + ts.TypeFlags.NumberLike, + (ignorePrimitives === true || ignorePrimitives!.string) && + ts.TypeFlags.StringLike, + ] + .filter((flag): flag is number => typeof flag === 'number') + .reduce((previous, flag) => previous | flag, 0); + if ( + type.flags !== ts.TypeFlags.Null && + type.flags !== ts.TypeFlags.Undefined && + (type as ts.UnionOrIntersectionType).types.some(t => + tsutils + .intersectionTypeParts(t) + .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), + ) + ) { + return; + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + + const barBarOperator = nullThrows( + context.sourceCode.getTokenAfter( + node.left, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === node.operator, + ), + NullThrowsReasons.MissingToken('operator', node.type), + ); + + function* fix( + fixer: TSESLint.RuleFixer, + ): IterableIterator { + if (isLogicalOrOperator(node.parent)) { + // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) + if ( + node.left.type === AST_NODE_TYPES.LogicalExpression && + !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({ + node: barBarOperator, + messageId: 'preferNullishOverOr', + data: { description, equals }, + suggest: [ + { + messageId: 'suggestNullish', + data: { equals }, + fix, + }, + ], + }); + } + return { + 'AssignmentExpression[operator = "||="]'( + node: TSESTree.AssignmentExpression, + ): void { + checkAssignmentOrLogicalExpression(node, 'assignment', '='); + }, ConditionalExpression(node: TSESTree.ConditionalExpression): void { if (ignoreTernaryTests) { return; @@ -200,7 +300,7 @@ export default 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 === '===' @@ -304,9 +404,12 @@ export default createRule({ context.report({ node, messageId: 'preferNullishOverTernary', + // TODO: also account for = in the ternary clause + data: { equals: '' }, suggest: [ { messageId: 'suggestNullish', + data: { equals: '' }, fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' @@ -325,90 +428,10 @@ export default createRule({ }); } }, - 'LogicalExpression[operator = "||"]'( node: TSESTree.LogicalExpression, ): void { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode.left); - if (!isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined)) { - return; - } - - if (ignoreConditionalTests === true && isConditionalTest(node)) { - return; - } - - const isMixedLogical = isMixedLogicalExpression(node); - if (ignoreMixedLogicalExpressions === true && isMixedLogical) { - return; - } - - // https://github.com/typescript-eslint/typescript-eslint/issues/5439 - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const ignorableFlags = [ - (ignorePrimitives === true || ignorePrimitives!.bigint) && - ts.TypeFlags.BigIntLike, - (ignorePrimitives === true || ignorePrimitives!.boolean) && - ts.TypeFlags.BooleanLike, - (ignorePrimitives === true || ignorePrimitives!.number) && - ts.TypeFlags.NumberLike, - (ignorePrimitives === true || ignorePrimitives!.string) && - ts.TypeFlags.StringLike, - ] - .filter((flag): flag is number => typeof flag === 'number') - .reduce((previous, flag) => previous | flag, 0); - if ( - type.flags !== ts.TypeFlags.Null && - type.flags !== ts.TypeFlags.Undefined && - (type as ts.UnionOrIntersectionType).types.some(t => - tsutils - .intersectionTypeParts(t) - .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)), - ) - ) { - return; - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - - const barBarOperator = nullThrows( - context.sourceCode.getTokenAfter( - node.left, - token => - token.type === AST_TOKEN_TYPES.Punctuator && - token.value === node.operator, - ), - NullThrowsReasons.MissingToken('operator', node.type), - ); - - function* fix( - fixer: TSESLint.RuleFixer, - ): IterableIterator { - if (isLogicalOrOperator(node.parent)) { - // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) - if ( - node.left.type === AST_NODE_TYPES.LogicalExpression && - !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', ''); }, }; }, @@ -451,7 +474,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) { @@ -463,7 +488,7 @@ function isMixedLogicalExpression(node: TSESTree.LogicalExpression): boolean { if (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 85c2365739c0..d30d7e21b178 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -30,31 +30,42 @@ const nullishTypes = ['null', 'undefined', 'null | undefined']; const ignorablePrimitiveTypes = ['string', 'number', 'boolean', 'bigint']; function typeValidTest( - cb: (type: string) => string | ValidTestCase, + cb: (type: string, equals: '' | '=') => string | ValidTestCase, ): (string | ValidTestCase)[] { - return types.map(type => cb(type)); + return [ + ...types.map(type => cb(type, '')), + ...types.map(type => cb(type, '=')), + ]; } -function nullishTypeTest< + +const nullishTypeTest = < T extends | string | InvalidTestCase | ValidTestCase, ->(cb: (nullish: string, type: string) => T): T[] { - return nullishTypes.flatMap(nullish => types.map(type => cb(nullish, type))); -} +>( + cb: (nullish: string, type: string, equals: string) => T, +): T[] => + nullishTypes.flatMap(nullish => + types.flatMap(type => + ['', ...(cb.length === 3 ? ['='] : [])].map(equals => + cb(nullish, type, equals), + ), + ), + ); ruleTester.run('prefer-nullish-coalescing', rule, { valid: [ ...typeValidTest( - type => ` -declare const x: ${type}; -x || 'foo'; + (type, equals) => ` +declare let x: ${type}; +(x ||${equals} 'foo'); `, ), ...nullishTypeTest( - (nullish, type) => ` -declare const x: ${type} | ${nullish}; -x ?? 'foo'; + (nullish, type, equals) => ` +declare let x: ${type} | ${nullish}; +x ??${equals} 'foo'; `, ), @@ -87,31 +98,31 @@ 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; `, ].map(code => ({ @@ -120,113 +131,113 @@ x === null ? x : y; })), // ignoreConditionalTests - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; `, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (x || 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} `, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -do {} while (x || 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) `, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -for (;x || 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} `, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -while (x || 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} `, })), // ignoreMixedLogicalExpressions ...nullishTypeTest((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 }], })), ...nullishTypeTest((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 }], })), ...nullishTypeTest((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 }], })), ...ignorablePrimitiveTypes.map>(type => ({ code: ` -declare const x: ${type} | undefined; +declare let x: ${type} | undefined; x || y; `, options: [{ ignorePrimitives: { [type]: true } }], })), ...ignorablePrimitiveTypes.map>(type => ({ code: ` -declare const x: ${type} | undefined; +declare let x: ${type} | undefined; x || y; `, options: [{ ignorePrimitives: true }], })), ...ignorablePrimitiveTypes.map>(type => ({ code: ` -declare const x: (${type} & { __brand?: any }) | undefined; +declare let x: (${type} & { __brand?: any }) | undefined; x || y; `, options: [{ ignorePrimitives: { [type]: true } }], })), ...ignorablePrimitiveTypes.map>(type => ({ code: ` -declare const x: (${type} & { __brand?: any }) | undefined; +declare let x: (${type} & { __brand?: any }) | undefined; x || y; `, options: [{ ignorePrimitives: true }], })), ` - declare const x: any; - declare const y: number; + declare let x: any; + declare let y: number; x || y; `, ` - declare const x: unknown; - declare const y: number; + declare let x: unknown; + declare let y: number; x || y; `, ` - declare const x: never; - declare const y: number; + declare let x: never; + declare let y: number; x || y; `, { code: ` -declare const x: 0 | 1 | 0n | 1n | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x || y; `, options: [ @@ -242,7 +253,7 @@ x || y; }, { code: ` -declare const x: 0 | 1 | 0n | 1n | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x || y; `, options: [ @@ -258,7 +269,7 @@ x || y; }, { code: ` -declare const x: 0 | 'foo' | undefined; +declare let x: 0 | 'foo' | undefined; x || y; `, options: [ @@ -272,7 +283,7 @@ x || y; }, { code: ` -declare const x: 0 | 'foo' | undefined; +declare let x: 0 | 'foo' | undefined; x || y; `, options: [ @@ -291,7 +302,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x || y; `, options: [ @@ -309,7 +320,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x || y; `, options: [ @@ -327,7 +338,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x || y; `, options: [ @@ -345,7 +356,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x || y; `, options: [ @@ -358,15 +369,15 @@ x || y; }, ], invalid: [ - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo'; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo'); `, errors: [ { - column: 3, - endColumn: 5, + column: 4, + endColumn: 6 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -374,8 +385,8 @@ x || 'foo'; { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo'; +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo'); `, }, ], @@ -501,35 +512,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; x === undefined ? 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; x === null ? y : x; `, ` -declare const x: string | null; +declare let x: string | null; null === x ? y : x; `, ].map(code => ({ @@ -559,7 +570,7 @@ x ?? y; // noStrictNullCheck { code: ` -declare const x: string[] | null; +declare let x: string[] | null; if (x) { } `, @@ -579,15 +590,15 @@ if (x) { }, // ignoreConditionalTests - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -x || 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ||${equals} 'foo') ? null : null; `, errors: [ { - column: 3, - endColumn: 5, + column: 4, + endColumn: 6 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -595,8 +606,8 @@ x || 'foo' ? null : null; { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo' ? null : null; +declare let x: ${type} | ${nullish}; +(x ??${equals} 'foo') ? null : null; `, }, ], @@ -605,15 +616,15 @@ x ?? 'foo' ? null : null; options: [{ ignoreConditionalTests: false }], output: null, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (x || 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ||${equals} 'foo')) {} `, errors: [ { - column: 7, - endColumn: 9, + column: 8, + endColumn: 10 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -621,8 +632,8 @@ if (x || 'foo') {} { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -if (x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +if ((x ??${equals} 'foo')) {} `, }, ], @@ -631,15 +642,15 @@ if (x ?? 'foo') {} options: [{ ignoreConditionalTests: false }], output: null, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -do {} while (x || 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ||${equals} 'foo')) `, errors: [ { - column: 16, - endColumn: 18, + column: 17, + endColumn: 19 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -647,8 +658,8 @@ do {} while (x || 'foo') { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -do {} while (x ?? 'foo') +declare let x: ${type} | ${nullish}; +do {} while ((x ??${equals} 'foo')) `, }, ], @@ -657,15 +668,15 @@ do {} while (x ?? 'foo') options: [{ ignoreConditionalTests: false }], output: null, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -for (;x || 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ||${equals} 'foo');) {} `, errors: [ { - column: 9, - endColumn: 11, + column: 10, + endColumn: 12 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -673,8 +684,8 @@ for (;x || 'foo';) {} { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -for (;x ?? 'foo';) {} +declare let x: ${type} | ${nullish}; +for (;(x ??${equals} 'foo');) {} `, }, ], @@ -683,15 +694,15 @@ for (;x ?? 'foo';) {} options: [{ ignoreConditionalTests: false }], output: null, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -while (x || 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ||${equals} 'foo')) {} `, errors: [ { - column: 10, - endColumn: 12, + column: 11, + endColumn: 13 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -699,8 +710,8 @@ while (x || 'foo') {} { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -while (x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +while ((x ??${equals} 'foo')) {} `, }, ], @@ -713,9 +724,9 @@ while (x ?? 'foo') {} // ignoreMixedLogicalExpressions ...nullishTypeTest((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; `, errors: [ @@ -729,9 +740,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; `, }, @@ -742,10 +753,10 @@ a ?? b && c; })), ...nullishTypeTest((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; `, errors: [ @@ -759,10 +770,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; `, }, @@ -778,10 +789,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; `, }, @@ -792,10 +803,10 @@ a || b ?? c && d; })), ...nullishTypeTest((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; `, errors: [ @@ -809,10 +820,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; `, }, @@ -828,10 +839,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; `, }, @@ -842,15 +853,15 @@ a && b || c ?? d; })), // should not false positive for functions inside conditional tests - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((nullish, type, equals) => ({ code: ` -declare const x: ${type} | ${nullish}; -if (() => x || 'foo') {} +declare let x: ${type} | ${nullish}; +if (() => (x ||${equals} 'foo')) {} `, errors: [ { - column: 13, - endColumn: 15, + column: 14, + endColumn: 16 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -858,8 +869,8 @@ if (() => x || 'foo') {} { messageId: 'suggestNullish', output: ` -declare const x: ${type} | ${nullish}; -if (() => x ?? 'foo') {} +declare let x: ${type} | ${nullish}; +if (() => (x ??${equals} 'foo')) {} `, }, ], @@ -867,15 +878,15 @@ if (() => x ?? 'foo') {} ], output: null, })), - ...nullishTypeTest((nullish, type) => ({ + ...nullishTypeTest((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') }) {} `, errors: [ { - column: 33, - endColumn: 35, + column: 34, + endColumn: 36 + equals.length, endLine: 3, line: 3, messageId: 'preferNullishOverOr', @@ -883,8 +894,8 @@ if (function werid() { return x || 'foo' }) {} { 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') }) {} `, }, ], @@ -895,9 +906,9 @@ if (function werid() { return x ?? 'foo' }) {} // https://github.com/typescript-eslint/typescript-eslint/issues/1290 ...nullishTypeTest((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; `, errors: [ @@ -911,9 +922,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; `, }, @@ -925,7 +936,7 @@ declare const c: ${type}; // default for missing option { code: ` -declare const x: string | undefined; +declare let x: string | undefined; x || y; `, errors: [ @@ -935,7 +946,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: string | undefined; +declare let x: string | undefined; x ?? y; `, }, @@ -951,7 +962,7 @@ x ?? y; }, { code: ` -declare const x: number | undefined; +declare let x: number | undefined; x || y; `, errors: [ @@ -961,7 +972,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: number | undefined; +declare let x: number | undefined; x ?? y; `, }, @@ -977,7 +988,7 @@ x ?? y; }, { code: ` -declare const x: boolean | undefined; +declare let x: boolean | undefined; x || y; `, errors: [ @@ -987,7 +998,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: boolean | undefined; +declare let x: boolean | undefined; x ?? y; `, }, @@ -1003,7 +1014,7 @@ x ?? y; }, { code: ` -declare const x: bigint | undefined; +declare let x: bigint | undefined; x || y; `, errors: [ @@ -1013,7 +1024,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: bigint | undefined; +declare let x: bigint | undefined; x ?? y; `, }, @@ -1030,7 +1041,7 @@ x ?? y; // falsy { code: ` -declare const x: '' | undefined; +declare let x: '' | undefined; x || y; `, errors: [ @@ -1040,7 +1051,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: '' | undefined; +declare let x: '' | undefined; x ?? y; `, }, @@ -1061,7 +1072,7 @@ x ?? y; }, { code: ` -declare const x: \`\` | undefined; +declare let x: \`\` | undefined; x || y; `, errors: [ @@ -1071,7 +1082,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: \`\` | undefined; +declare let x: \`\` | undefined; x ?? y; `, }, @@ -1092,7 +1103,7 @@ x ?? y; }, { code: ` -declare const x: 0 | undefined; +declare let x: 0 | undefined; x || y; `, errors: [ @@ -1102,7 +1113,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0 | undefined; +declare let x: 0 | undefined; x ?? y; `, }, @@ -1123,7 +1134,7 @@ x ?? y; }, { code: ` -declare const x: 0n | undefined; +declare let x: 0n | undefined; x || y; `, errors: [ @@ -1133,7 +1144,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0n | undefined; +declare let x: 0n | undefined; x ?? y; `, }, @@ -1154,7 +1165,7 @@ x ?? y; }, { code: ` -declare const x: false | undefined; +declare let x: false | undefined; x || y; `, errors: [ @@ -1164,7 +1175,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: false | undefined; +declare let x: false | undefined; x ?? y; `, }, @@ -1186,7 +1197,7 @@ x ?? y; // truthy { code: ` -declare const x: 'a' | undefined; +declare let x: 'a' | undefined; x || y; `, errors: [ @@ -1196,7 +1207,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 'a' | undefined; +declare let x: 'a' | undefined; x ?? y; `, }, @@ -1217,7 +1228,7 @@ x ?? y; }, { code: ` -declare const x: \`hello\${'string'}\` | undefined; +declare let x: \`hello\${'string'}\` | undefined; x || y; `, errors: [ @@ -1227,7 +1238,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: \`hello\${'string'}\` | undefined; +declare let x: \`hello\${'string'}\` | undefined; x ?? y; `, }, @@ -1248,7 +1259,7 @@ x ?? y; }, { code: ` -declare const x: 1 | undefined; +declare let x: 1 | undefined; x || y; `, errors: [ @@ -1258,7 +1269,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 1 | undefined; +declare let x: 1 | undefined; x ?? y; `, }, @@ -1279,7 +1290,7 @@ x ?? y; }, { code: ` -declare const x: 1n | undefined; +declare let x: 1n | undefined; x || y; `, errors: [ @@ -1289,7 +1300,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 1n | undefined; +declare let x: 1n | undefined; x ?? y; `, }, @@ -1310,7 +1321,7 @@ x ?? y; }, { code: ` -declare const x: true | undefined; +declare let x: true | undefined; x || y; `, errors: [ @@ -1320,7 +1331,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: true | undefined; +declare let x: true | undefined; x ?? y; `, }, @@ -1342,7 +1353,7 @@ x ?? y; // Unions of same primitive { code: ` -declare const x: 'a' | 'b' | undefined; +declare let x: 'a' | 'b' | undefined; x || y; `, errors: [ @@ -1352,7 +1363,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 'a' | 'b' | undefined; +declare let x: 'a' | 'b' | undefined; x ?? y; `, }, @@ -1373,7 +1384,7 @@ x ?? y; }, { code: ` -declare const x: 'a' | \`b\` | undefined; +declare let x: 'a' | \`b\` | undefined; x || y; `, errors: [ @@ -1383,7 +1394,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 'a' | \`b\` | undefined; +declare let x: 'a' | \`b\` | undefined; x ?? y; `, }, @@ -1404,7 +1415,7 @@ x ?? y; }, { code: ` -declare const x: 0 | 1 | undefined; +declare let x: 0 | 1 | undefined; x || y; `, errors: [ @@ -1414,7 +1425,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0 | 1 | undefined; +declare let x: 0 | 1 | undefined; x ?? y; `, }, @@ -1435,7 +1446,7 @@ x ?? y; }, { code: ` -declare const x: 1 | 2 | 3 | undefined; +declare let x: 1 | 2 | 3 | undefined; x || y; `, errors: [ @@ -1445,7 +1456,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 1 | 2 | 3 | undefined; +declare let x: 1 | 2 | 3 | undefined; x ?? y; `, }, @@ -1466,7 +1477,7 @@ x ?? y; }, { code: ` -declare const x: 0n | 1n | undefined; +declare let x: 0n | 1n | undefined; x || y; `, errors: [ @@ -1476,7 +1487,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0n | 1n | undefined; +declare let x: 0n | 1n | undefined; x ?? y; `, }, @@ -1497,7 +1508,7 @@ x ?? y; }, { code: ` -declare const x: 1n | 2n | 3n | undefined; +declare let x: 1n | 2n | 3n | undefined; x || y; `, errors: [ @@ -1507,7 +1518,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 1n | 2n | 3n | undefined; +declare let x: 1n | 2n | 3n | undefined; x ?? y; `, }, @@ -1528,7 +1539,7 @@ x ?? y; }, { code: ` -declare const x: true | false | undefined; +declare let x: true | false | undefined; x || y; `, errors: [ @@ -1538,7 +1549,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: true | false | undefined; +declare let x: true | false | undefined; x ?? y; `, }, @@ -1560,7 +1571,7 @@ x ?? y; // Mixed unions { code: ` -declare const x: 0 | 1 | 0n | 1n | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x || y; `, errors: [ @@ -1570,7 +1581,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: 0 | 1 | 0n | 1n | undefined; +declare let x: 0 | 1 | 0n | 1n | undefined; x ?? y; `, }, @@ -1591,7 +1602,7 @@ x ?? y; }, { code: ` -declare const x: true | false | null | undefined; +declare let x: true | false | null | undefined; x || y; `, errors: [ @@ -1601,7 +1612,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: true | false | null | undefined; +declare let x: true | false | null | undefined; x ?? y; `, }, @@ -1622,7 +1633,7 @@ x ?? y; }, { code: ` -declare const x: null; +declare let x: null; x || y; `, errors: [ @@ -1632,7 +1643,7 @@ x || y; { messageId: 'suggestNullish', output: ` -declare const x: null; +declare let x: null; x ?? y; `, }, @@ -1707,7 +1718,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x || y; `, errors: [ @@ -1722,7 +1733,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x ?? y; `, }, @@ -1738,7 +1749,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x || y; `, errors: [ @@ -1753,7 +1764,7 @@ enum Enum { B = 1, C = 2, } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x ?? y; `, }, @@ -1769,7 +1780,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x || y; `, errors: [ @@ -1784,7 +1795,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum | undefined; +declare let x: Enum | undefined; x ?? y; `, }, @@ -1800,7 +1811,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x || y; `, errors: [ @@ -1815,7 +1826,7 @@ enum Enum { B = 'b', C = 'c', } -declare const x: Enum.A | Enum.B | undefined; +declare let x: Enum.A | Enum.B | undefined; x ?? y; `, }, diff --git a/packages/rule-tester/src/utils/config-validator.ts b/packages/rule-tester/src/utils/config-validator.ts index b8b740915aba..bc5e09e7646c 100644 --- a/packages/rule-tester/src/utils/config-validator.ts +++ b/packages/rule-tester/src/utils/config-validator.ts @@ -197,7 +197,7 @@ function validateConfigSchema( config: TesterConfigWithDefaults, source: string, ): void { - validateSchema ||= ajv.compile(flatConfigSchema); + validateSchema ??= ajv.compile(flatConfigSchema); if (!validateSchema(config)) { throw new Error( diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 0d476b38a7d0..21c1871cba65 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -18,7 +18,7 @@ export interface FileSpecifier { /** * Type or value name(s) to match on. */ - name: string[] | string; + name: string | string[]; /** * A specific file the types or values must be declared in. @@ -36,7 +36,7 @@ export interface LibSpecifier { /** * Type or value name(s) to match on. */ - name: string[] | string; + name: string | string[]; } /** @@ -49,7 +49,7 @@ export interface PackageSpecifier { /** * Type or value name(s) to match on. */ - name: string[] | string; + name: string | string[]; /** * Package name the type or value must be declared in. @@ -62,10 +62,10 @@ export interface PackageSpecifier { * See [TypeOrValueSpecifier](/packages/type-utils/type-or-value-specifier). */ export type TypeOrValueSpecifier = + | string | FileSpecifier | LibSpecifier - | PackageSpecifier - | string; + | PackageSpecifier; export const typeOrValueSpecifiersSchema = { items: { diff --git a/packages/type-utils/src/builtinSymbolLikes.ts b/packages/type-utils/src/builtinSymbolLikes.ts index f8c27524c365..d1833e3ada84 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -121,7 +121,7 @@ export function isBuiltinTypeAliasLike( export function isBuiltinSymbolLike( program: ts.Program, type: ts.Type, - symbolName: string[] | string, + symbolName: string | string[], ): boolean { return isBuiltinSymbolLikeRecurser(program, type, subType => { const symbol = subType.getSymbol(); diff --git a/packages/type-utils/src/typeOrValueSpecifiers/specifierNameMatches.ts b/packages/type-utils/src/typeOrValueSpecifiers/specifierNameMatches.ts index bd26509c1e12..278ade8b8f17 100644 --- a/packages/type-utils/src/typeOrValueSpecifiers/specifierNameMatches.ts +++ b/packages/type-utils/src/typeOrValueSpecifiers/specifierNameMatches.ts @@ -4,7 +4,7 @@ import * as tsutils from 'ts-api-utils'; export function specifierNameMatches( type: ts.Type, - names: string[] | string, + names: string | string[], ): boolean { if (typeof names === 'string') { names = [names]; diff --git a/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts b/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts index f10c2b9635f2..592c45b1520b 100644 --- a/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts +++ b/packages/type-utils/tests/getConstrainedTypeAtLocation.test.ts @@ -11,11 +11,11 @@ const mockType = (): ts.Type => { }; const mockServices = ({ - typeAtLocation, baseConstraintOfType, + typeAtLocation, }: { - typeAtLocation: ts.Type; baseConstraintOfType?: ts.Type; + typeAtLocation: ts.Type; }): ParserServicesWithTypeInformation => { const typeChecker = { getBaseConstraintOfType: (_: ts.Type) => baseConstraintOfType, @@ -25,8 +25,8 @@ const mockServices = ({ } as ts.Program; return { - program, getTypeAtLocation: (_: TSESTree.Node) => typeAtLocation, + program, } as ParserServicesWithTypeInformation; }; @@ -36,8 +36,8 @@ describe('getConstrainedTypeAtLocation', () => { const typeAtLocation = mockType(); const baseConstraintOfType = mockType(); const services = mockServices({ - typeAtLocation, baseConstraintOfType, + typeAtLocation, }); expect(getConstrainedTypeAtLocation(services, node)).toBe(