From 52b60852d0ba6bb6abe519c9d3ec1b231793e91d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 17 May 2020 10:57:41 -0700 Subject: [PATCH] feat(eslint-plugin): [prefer-nullish-coalescing][prefer-optional-chain] remove unsafe fixers These fixers are very unsafe and even providing default options for them are a bad idea. This ensures that they are suggestion fixers in all cases, and removes the options that allow them not to be. --- packages/eslint-plugin/README.md | 4 +- .../docs/rules/prefer-nullish-coalescing.md | 8 - .../docs/rules/prefer-optional-chain.md | 15 +- .../src/rules/prefer-nullish-coalescing.ts | 37 +--- .../src/rules/prefer-optional-chain.ts | 63 ++---- .../rules/prefer-nullish-coalescing.test.ts | 174 +++++++++------- .../tests/rules/prefer-optional-chain.test.ts | 185 ++++++++++++++---- 7 files changed, 271 insertions(+), 215 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index fe165f2bc9f..c2d4531eae0 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -146,8 +146,8 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | | [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | :wrench: | | +| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | | :thought_balloon: | +| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | | | | [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are 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) | Requires that function parameters are typed as readonly to prevent accidental mutation of inputs | | | :thought_balloon: | | [`@typescript-eslint/prefer-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Prefer using type parameter when calling `Array#reduce` instead of casting | | :wrench: | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md index 5b99e5c613f..bb32c02895b 100644 --- a/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md +++ b/packages/eslint-plugin/docs/rules/prefer-nullish-coalescing.md @@ -46,7 +46,6 @@ type Options = [ { ignoreConditionalTests?: boolean; ignoreMixedLogicalExpressions?: boolean; - forceSuggestionFixer?: boolean; }, ]; @@ -54,7 +53,6 @@ const defaultOptions = [ { ignoreConditionalTests: true, ignoreMixedLogicalExpressions: true, - forceSuggestionFixer: false, }, ]; ``` @@ -133,12 +131,6 @@ a ?? (b && c && d); **_NOTE:_** Errors for this specific case will be presented as suggestions (see below), instead of fixes. This is because it is not always safe to automatically convert `||` to `??` within a mixed logical expression, as we cannot tell the intended precedence of the operator. Note that by design, `??` requires parentheses when used with `&&` or `||` in the same expression. -### `forceSuggestionFixer` - -Setting this option to `true` will cause the rule to use ESLint's "suggested fix" mode for all fixes. _This option is provided as to aid in transitioning your codebase onto this rule_. - -Suggestion fixes cannot be automatically applied via the `--fix` CLI command, but can be _manually_ chosen to be applied one at a time via an IDE or similar. This makes it safe to run autofixers on an existing codebase without worrying about potential runtime behavior changes from this rule's fixer. - ## When Not To Use It If you are not using TypeScript 3.7 (or greater), then you will not be able to use this rule, as the operator is not supported. diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md index be3352a282b..6505c93959a 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md @@ -70,20 +70,7 @@ foo?.a?.b?.method?.(); foo?.a?.b?.c?.d?.e; ``` -## Options - -The rule accepts an options object with the following properties: - -```ts -type Options = { - // if true, the rule will only provide suggested fixes instead of automatically modifying code - suggestInsteadOfAutofix?: boolean; -}; - -const defaults = { - suggestInsteadOfAutofix: false, -}; -``` +**Note:** there are a few edge cases where this rule will false positive. Use your best judgement when evaluating reported errors. ## When Not To Use It diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 3c616ded75d..fa6c1bb4e21 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -10,10 +10,9 @@ export type Options = [ { ignoreConditionalTests?: boolean; ignoreMixedLogicalExpressions?: boolean; - forceSuggestionFixer?: boolean; }, ]; -export type MessageIds = 'preferNullish'; +export type MessageIds = 'preferNullish' | 'suggestNullish'; export default util.createRule({ name: 'prefer-nullish-coalescing', @@ -27,10 +26,10 @@ export default util.createRule({ suggestion: true, requiresTypeChecking: true, }, - fixable: 'code', messages: { preferNullish: 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', + suggestNullish: 'Fix to nullish coalescing operator (`??`).', }, schema: [ { @@ -54,19 +53,9 @@ export default util.createRule({ { ignoreConditionalTests: true, ignoreMixedLogicalExpressions: true, - forceSuggestionFixer: false, }, ], - create( - context, - [ - { - ignoreConditionalTests, - ignoreMixedLogicalExpressions, - forceSuggestionFixer, - }, - ], - ) { + create(context, [{ ignoreConditionalTests, ignoreMixedLogicalExpressions }]) { const parserServices = util.getParserServices(context); const sourceCode = context.getSourceCode(); const checker = parserServices.program.getTypeChecker(); @@ -119,23 +108,15 @@ export default util.createRule({ yield fixer.replaceText(barBarOperator, '??'); } - const fixer = - isMixedLogical || forceSuggestionFixer - ? // suggestion instead for cases where we aren't sure if the fixer is completely safe - ({ - suggest: [ - { - messageId: 'preferNullish', - fix, - }, - ], - } as const) - : { fix }; - context.report({ node: barBarOperator, messageId: 'preferNullish', - ...fixer, + suggest: [ + { + messageId: 'suggestNullish', + fix, + }, + ], }); }, }; diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 0d5176b6f10..c2d0db90ea7 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -31,15 +31,7 @@ The AST will look like this: } */ -type Options = [ - { - suggestInsteadOfAutofix?: boolean; - }, -]; - -type MessageIds = 'preferOptionalChain' | 'optionalChainSuggest'; - -export default util.createRule({ +export default util.createRule({ name: 'prefer-optional-chain', meta: { type: 'suggestion', @@ -50,30 +42,15 @@ export default util.createRule({ recommended: false, suggestion: true, }, - fixable: 'code', messages: { preferOptionalChain: "Prefer using an optional chain expression instead, as it's more concise and easier to read.", optionalChainSuggest: 'Change to an optional chain.', }, - schema: [ - { - type: 'object', - properties: { - suggestInsteadOfAutofix: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], + schema: [], }, - defaultOptions: [ - { - suggestInsteadOfAutofix: false, - }, - ], - create(context, [options]) { + defaultOptions: [], + create(context) { const sourceCode = context.getSourceCode(); return { [[ @@ -189,28 +166,18 @@ export default util.createRule({ } ${sourceCode.getText(previous.right.right)}`; } - if (!options.suggestInsteadOfAutofix) { - context.report({ - node: previous, - messageId: 'preferOptionalChain', - fix(fixer) { - return fixer.replaceText(previous, optionallyChainedCode); + context.report({ + node: previous, + messageId: 'preferOptionalChain', + suggest: [ + { + messageId: 'optionalChainSuggest', + fix: (fixer): TSESLint.RuleFix[] => [ + fixer.replaceText(previous, optionallyChainedCode), + ], }, - }); - } else { - context.report({ - node: previous, - messageId: 'preferOptionalChain', - suggest: [ - { - messageId: 'optionalChainSuggest', - fix: (fixer): TSESLint.RuleFix[] => [ - fixer.replaceText(previous, optionallyChainedCode), - ], - }, - ], - }); - } + ], + }); } }, }; 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 40a8916317c..a984484edd5 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -144,11 +144,8 @@ a && b || c || d; code: ` declare const x: ${type} | ${nullish}; x || 'foo'; - `, - output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo'; - `, + `.trimRight(), + output: null, errors: [ { messageId: 'preferNullish', @@ -156,6 +153,15 @@ x ?? 'foo'; column: 3, endLine: 3, endColumn: 5, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +x ?? 'foo'; + `.trimRight(), + }, + ], }, ], })), @@ -165,11 +171,8 @@ x ?? 'foo'; code: ` declare const x: ${type} | ${nullish}; x || 'foo' ? null : null; - `, - output: ` -declare const x: ${type} | ${nullish}; -x ?? 'foo' ? null : null; - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: false }], errors: [ { @@ -178,6 +181,15 @@ x ?? 'foo' ? null : null; column: 3, endLine: 3, endColumn: 5, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +x ?? 'foo' ? null : null; + `.trimRight(), + }, + ], }, ], })), @@ -185,11 +197,8 @@ x ?? 'foo' ? null : null; code: ` declare const x: ${type} | ${nullish}; if (x || 'foo') {} - `, - output: ` -declare const x: ${type} | ${nullish}; -if (x ?? 'foo') {} - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: false }], errors: [ { @@ -198,6 +207,15 @@ if (x ?? 'foo') {} column: 7, endLine: 3, endColumn: 9, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +if (x ?? 'foo') {} + `.trimRight(), + }, + ], }, ], })), @@ -205,11 +223,8 @@ if (x ?? 'foo') {} code: ` declare const x: ${type} | ${nullish}; do {} while (x || 'foo') - `, - output: ` -declare const x: ${type} | ${nullish}; -do {} while (x ?? 'foo') - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: false }], errors: [ { @@ -218,6 +233,15 @@ do {} while (x ?? 'foo') column: 16, endLine: 3, endColumn: 18, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +do {} while (x ?? 'foo') + `.trimRight(), + }, + ], }, ], })), @@ -225,11 +249,8 @@ do {} while (x ?? 'foo') code: ` declare const x: ${type} | ${nullish}; for (;x || 'foo';) {} - `, - output: ` -declare const x: ${type} | ${nullish}; -for (;x ?? 'foo';) {} - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: false }], errors: [ { @@ -238,6 +259,15 @@ for (;x ?? 'foo';) {} column: 9, endLine: 3, endColumn: 11, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +for (;x ?? 'foo';) {} + `.trimRight(), + }, + ], }, ], })), @@ -245,11 +275,8 @@ for (;x ?? 'foo';) {} code: ` declare const x: ${type} | ${nullish}; while (x || 'foo') {} - `, - output: ` -declare const x: ${type} | ${nullish}; -while (x ?? 'foo') {} - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: false }], errors: [ { @@ -258,6 +285,15 @@ while (x ?? 'foo') {} column: 10, endLine: 3, endColumn: 12, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +while (x ?? 'foo') {} + `.trimRight(), + }, + ], }, ], })), @@ -280,7 +316,7 @@ a || b && c; endColumn: 5, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; @@ -310,7 +346,7 @@ a || b || c && d; endColumn: 5, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; @@ -329,7 +365,7 @@ declare const d: ${type} | ${nullish}; endColumn: 10, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; @@ -360,7 +396,7 @@ a && b || c || d; endColumn: 10, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; @@ -379,7 +415,7 @@ a && (b ?? c) || d; endColumn: 15, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; @@ -398,11 +434,8 @@ a && b || c ?? d; code: ` declare const x: ${type} | ${nullish}; if (() => x || 'foo') {} - `, - output: ` -declare const x: ${type} | ${nullish}; -if (() => x ?? 'foo') {} - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: true }], errors: [ { @@ -411,6 +444,15 @@ if (() => x ?? 'foo') {} column: 13, endLine: 3, endColumn: 15, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: ${type} | ${nullish}; +if (() => x ?? 'foo') {} + `.trimRight(), + }, + ], }, ], })), @@ -418,11 +460,8 @@ if (() => x ?? 'foo') {} code: ` declare const x: ${type} | ${nullish}; if (function werid() { return x || 'foo' }) {} - `, - output: ` -declare const x: ${type} | ${nullish}; -if (function werid() { return x ?? 'foo' }) {} - `, + `.trimRight(), + output: null, options: [{ ignoreConditionalTests: true }], errors: [ { @@ -431,37 +470,18 @@ if (function werid() { return x ?? 'foo' }) {} column: 33, endLine: 3, endColumn: 35, - }, - ], - })), - - // testing the suggestion fixer option - { - code: ` -declare const x: string | null; -x || 'foo'; - `.trimRight(), - output: null, - options: [{ forceSuggestionFixer: true }], - errors: [ - { - messageId: 'preferNullish', - line: 3, - column: 3, - endLine: 3, - endColumn: 5, suggestions: [ { - messageId: 'preferNullish', + messageId: 'suggestNullish', output: ` -declare const x: string | null; -x ?? 'foo'; +declare const x: ${type} | ${nullish}; +if (function werid() { return x ?? 'foo' }) {} `.trimRight(), }, ], }, ], - }, + })), // https://github.com/typescript-eslint/typescript-eslint/issues/1290 ...nullishTypeInvalidTest((nullish, type) => ({ @@ -471,12 +491,7 @@ declare const b: ${type}; declare const c: ${type}; a || b || c; `.trimRight(), - output: ` -declare const a: ${type} | ${nullish}; -declare const b: ${type}; -declare const c: ${type}; -(a ?? b) || c; - `.trimRight(), + output: null, errors: [ { messageId: 'preferNullish', @@ -484,6 +499,17 @@ declare const c: ${type}; column: 3, endLine: 5, endColumn: 5, + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type}; +declare const c: ${type}; +(a ?? b) || c; + `.trimRight(), + }, + ], }, ], })), diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index c6eb323827a..fa3404bfe6f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -80,12 +80,6 @@ const baseCases = [ 'foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz] && foo.bar.baz[buzz]()', output: 'foo?.bar?.baz?.[buzz]?.()', }, - // two-for-one - { - code: 'foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo', - output: 'foo?.bar?.baz || baz?.bar?.foo', - errors: 2, - }, // (partially) pre-optional chained { code: @@ -104,10 +98,18 @@ const baseCases = [ c => ({ code: c.code.trim(), - output: c.output.trim(), - errors: Array(c.errors ?? 1).fill({ - messageId: 'preferOptionalChain', - }), + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: c.output.trim(), + }, + ], + }, + ], } as TSESLint.InvalidTestCase< InferMessageIdsTypeFromRule, InferOptionsTypeFromRule @@ -149,12 +151,32 @@ ruleTester.run('prefer-optional-chain', rule, { ...baseCases.map(c => ({ ...c, code: `${c.code} && bing`, - output: `${c.output} && bing`, + errors: [ + { + ...c.errors[0], + suggestions: [ + { + ...c.errors[0].suggestions![0], + output: `${c.errors[0].suggestions![0].output} && bing`, + }, + ], + }, + ], })), ...baseCases.map(c => ({ ...c, code: `${c.code} && bing.bong`, - output: `${c.output} && bing.bong`, + errors: [ + { + ...c.errors[0], + suggestions: [ + { + ...c.errors[0].suggestions![0], + output: `${c.errors[0].suggestions![0].output} && bing.bong`, + }, + ], + }, + ], })), // strict nullish equality checks x !== null && x.y !== null ...baseCases.map(c => ({ @@ -173,24 +195,61 @@ ruleTester.run('prefer-optional-chain', rule, { ...c, code: c.code.replace(/&&/g, '!= undefined &&'), })), + // two errors + { + code: noFormat`foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo`, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`foo?.bar?.baz || baz && baz.bar && baz.bar.foo`, + }, + ], + }, + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`foo && foo.bar && foo.bar.baz || baz?.bar?.foo`, + }, + ], + }, + ], + }, { // case with inconsistent checks code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', - output: 'foo?.bar?.baz?.buzz;', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.buzz;', + }, + ], }, ], }, // ensure essential whitespace isn't removed { code: 'foo && foo.bar(baz => );', - output: 'foo?.bar(baz => );', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar(baz => );', + }, + ], }, ], parserOptions: { @@ -201,93 +260,147 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: 'foo && foo.bar(baz => typeof baz);', - output: 'foo?.bar(baz => typeof baz);', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar(baz => typeof baz);', + }, + ], }, ], }, { code: noFormat`foo && foo["some long string"] && foo["some long string"].baz`, - output: noFormat`foo?.["some long string"]?.baz`, + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`foo?.["some long string"]?.baz`, + }, + ], }, ], }, { code: noFormat`foo && foo[\`some long string\`] && foo[\`some long string\`].baz`, - output: noFormat`foo?.[\`some long string\`]?.baz`, + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`foo?.[\`some long string\`]?.baz`, + }, + ], }, ], }, { code: "foo && foo['some long string'] && foo['some long string'].baz;", - output: "foo?.['some long string']?.baz;", + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: "foo?.['some long string']?.baz;", + }, + ], }, ], }, // should preserve comments in a call expression { code: noFormat` - foo && foo.bar(/* comment */a, - // comment2 - b, ); - `, - output: noFormat` - foo?.bar(/* comment */a, - // comment2 - b, ); - `, +foo && foo.bar(/* comment */a, + // comment2 + b, ); + `.trimRight(), + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat` +foo?.bar(/* comment */a, + // comment2 + b, ); + `.trimRight(), + }, + ], }, ], }, // ensure binary expressions that are the last expression do not get removed { code: 'foo && foo.bar != null;', - output: 'foo?.bar != null;', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar != null;', + }, + ], }, ], }, { code: 'foo && foo.bar != undefined;', - output: 'foo?.bar != undefined;', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar != undefined;', + }, + ], }, ], }, { code: 'foo && foo.bar != null && baz;', - output: 'foo?.bar != null && baz;', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar != null && baz;', + }, + ], }, ], }, // other weird cases { code: 'foo && foo?.();', - output: 'foo?.();', + output: null, errors: [ { messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.();', + }, + ], }, ], }, @@ -295,11 +408,6 @@ ruleTester.run('prefer-optional-chain', rule, { { code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', - options: [ - { - suggestInsteadOfAutofix: true, - }, - ], output: null, errors: [ { @@ -317,11 +425,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: 'foo && foo.bar(baz => );', - options: [ - { - suggestInsteadOfAutofix: true, - }, - ], output: null, errors: [ {