diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md index 4a9ada8b08ff..2772c0bb793e 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md @@ -57,9 +57,35 @@ foo?.a?.b?.c?.d?.e; -:::note -There are a few edge cases where this rule will false positive. Use your best judgement when evaluating reported errors. -::: +## Options + +### `looseFalsiness` + +By default, this rule will ignore cases that could result with different result values when switched to optional chaining. +For example, the following `box != null && box.value` would not be flagged by default. + + + +### ❌ Incorrect + +```ts +declare const box: { value: number } | null; +// Type: false | number +box != null && box.value; +``` + +### ✅ Correct + +```ts +declare const box: { value: number } | null; +// Type: undefined | number +box?.value; +``` + + + +If you don't mind your code considering all falsy values the same (e.g. the `false` and `undefined` above), you can enable `looseFalsiness: true`. +Doing so makes the rule slightly incorrect - but speeds it by not having to ask TypeScript's type checker for information. ## When Not To Use It diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index f02b53fcdf90..0907b2122427 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -35,6 +35,7 @@ export = { '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/prefer-includes': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/prefer-readonly': 'off', '@typescript-eslint/prefer-readonly-parameter-types': 'off', '@typescript-eslint/prefer-reduce-type-parameter': 'off', diff --git a/packages/eslint-plugin/src/configs/stylistic.ts b/packages/eslint-plugin/src/configs/stylistic.ts index ed5ce3ded8c9..d88f10090eb3 100644 --- a/packages/eslint-plugin/src/configs/stylistic.ts +++ b/packages/eslint-plugin/src/configs/stylistic.ts @@ -24,7 +24,6 @@ export = { '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', - '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/sort-type-constituents': 'error', }, }; diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index f3cbeb3b8423..be2c939c59f6 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -1,5 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import * as util from '../util'; @@ -38,6 +39,7 @@ export default util.createRule({ description: 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects', recommended: 'stylistic', + requiresTypeChecking: true, }, hasSuggestions: true, messages: { @@ -45,12 +47,101 @@ export default util.createRule({ "Prefer using an optional chain expression instead, as it's more concise and easier to read.", optionalChainSuggest: 'Change to an optional chain.', }, - schema: [], + schema: [ + { + type: 'object', + properties: { + looseFalsiness: { + description: 'Whether to consider all nullable values equivalent', + type: 'boolean', + }, + }, + }, + ], }, - defaultOptions: [], - create(context) { + defaultOptions: [ + { + looseFalsiness: false, + }, + ], + create(context, [{ looseFalsiness }]) { const sourceCode = context.getSourceCode(); - const services = util.getParserServices(context, true); + const services = util.getParserServices(context); + + interface ReportIfMoreThanOneOptions { + expressionCount: number; + initialNodeForType: TSESTree.Node; + previous: TSESTree.LogicalExpression; + optionallyChainedCode: string; + shouldHandleChainedAnds: boolean; + } + + function reportIfMoreThanOne({ + expressionCount, + initialNodeForType, + previous, + optionallyChainedCode, + shouldHandleChainedAnds, + }: ReportIfMoreThanOneOptions): void { + if (expressionCount <= 1) { + return; + } + + if (!looseFalsiness) { + const initialType = services.getTypeAtLocation(initialNodeForType); + + if ( + util.isTypeFlagSet( + initialType, + ts.TypeFlags.BigIntLike | + ts.TypeFlags.Null | + ts.TypeFlags.NumberLike | + ts.TypeFlags.StringLike, + ) || + tsutils + .unionTypeParts(initialType) + .some(subType => util.isTypeIntrinsic(subType, 'false')) + ) { + return; + } + } + + if ( + shouldHandleChainedAnds && + previous.right.type === AST_NODE_TYPES.BinaryExpression + ) { + let operator = previous.right.operator; + if ( + previous.right.operator === '!==' && + // TODO(#4820): Use the type checker to know whether this is `null` + previous.right.right.type === AST_NODE_TYPES.Literal && + previous.right.right.raw === 'null' + ) { + // case like foo !== null && foo.bar !== null + operator = '!='; + } + // case like foo && foo.bar !== someValue + optionallyChainedCode += ` ${operator} ${sourceCode.getText( + previous.right.right, + )}`; + } + + context.report({ + node: previous, + messageId: 'preferOptionalChain', + suggest: [ + { + messageId: 'optionalChainSuggest', + fix: (fixer): TSESLint.RuleFix[] => [ + fixer.replaceText( + previous, + `${shouldHandleChainedAnds ? '' : '!'}${optionallyChainedCode}`, + ), + ], + }, + ], + }); + } return { 'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'( @@ -187,10 +278,9 @@ export default util.createRule({ reportIfMoreThanOne({ expressionCount, + initialNodeForType: initialIdentifierOrNotEqualsExpr, previous, optionallyChainedCode, - sourceCode, - context, shouldHandleChainedAnds: false, }); }, @@ -271,10 +361,9 @@ export default util.createRule({ reportIfMoreThanOne({ expressionCount, + initialNodeForType: initialIdentifierOrNotEqualsExpr, previous, optionallyChainedCode, - sourceCode, - context, shouldHandleChainedAnds: true, }); }, @@ -472,67 +561,6 @@ const ALLOWED_NON_COMPUTED_PROP_TYPES: ReadonlySet = new Set([ AST_NODE_TYPES.PrivateIdentifier, ]); -interface ReportIfMoreThanOneOptions { - expressionCount: number; - previous: TSESTree.LogicalExpression; - optionallyChainedCode: string; - sourceCode: Readonly; - context: Readonly< - TSESLint.RuleContext< - 'preferOptionalChain' | 'optionalChainSuggest', - never[] - > - >; - shouldHandleChainedAnds: boolean; -} - -function reportIfMoreThanOne({ - expressionCount, - previous, - optionallyChainedCode, - sourceCode, - context, - shouldHandleChainedAnds, -}: ReportIfMoreThanOneOptions): void { - if (expressionCount > 1) { - if ( - shouldHandleChainedAnds && - previous.right.type === AST_NODE_TYPES.BinaryExpression - ) { - let operator = previous.right.operator; - if ( - previous.right.operator === '!==' && - // TODO(#4820): Use the type checker to know whether this is `null` - previous.right.right.type === AST_NODE_TYPES.Literal && - previous.right.right.raw === 'null' - ) { - // case like foo !== null && foo.bar !== null - operator = '!='; - } - // case like foo && foo.bar !== someValue - optionallyChainedCode += ` ${operator} ${sourceCode.getText( - previous.right.right, - )}`; - } - - context.report({ - node: previous, - messageId: 'preferOptionalChain', - suggest: [ - { - messageId: 'optionalChainSuggest', - fix: (fixer): TSESLint.RuleFix[] => [ - fixer.replaceText( - previous, - `${shouldHandleChainedAnds ? '' : '!'}${optionallyChainedCode}`, - ), - ], - }, - ], - }); - } -} - interface NormalizedPattern { invalidOptionallyChainedPrivateProperty: boolean; expressionCount: number; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index a18de12bf7b1..4d8cf7642485 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1,9 +1,14 @@ import rule from '../../../src/rules/prefer-optional-chain'; -import { noFormat, RuleTester } from '../../RuleTester'; -import * as BaseCases from './base-cases'; +import { getFixturesRootDir, noFormat, RuleTester } from '../../RuleTester'; + +const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, }); ruleTester.run('prefer-optional-chain', rule, { @@ -15,7 +20,6 @@ ruleTester.run('prefer-optional-chain', rule, { '!a.b || a.b?.();', '!a.b || a.b();', '!foo() || !foo().bar;', - 'foo || {};', 'foo || ({} as any);', '(foo || {})?.bar;', @@ -45,7 +49,6 @@ ruleTester.run('prefer-optional-chain', rule, { 'match && match$1 !== undefined;', 'foo !== null && foo !== undefined;', "x['y'] !== undefined && x['y'] !== null;", - // private properties 'this.#a && this.#b;', '!this.#a || !this.#b;', 'a.#foo?.bar;', @@ -54,6 +57,10 @@ ruleTester.run('prefer-optional-chain', rule, { '!a.b.#a || a;', '!new A().#b || a;', '!(await a).#b || a;', + // Do not handle direct optional chaining on private properties because of a typescript bug (https://github.com/microsoft/TypeScript/issues/42734) + // We still allow in computed properties + 'foo && foo.#bar;', + '!foo || !foo.#bar;', "!(foo as any).bar || 'anything';", // currently do not handle complex computed properties 'foo && foo[bar as string] && foo[bar as string].baz;', @@ -78,94 +85,754 @@ ruleTester.run('prefer-optional-chain', rule, { '!import.meta && !import.meta.foo;', 'new.target || new.target.length;', '!new.target || true;', - // Do not handle direct optional chaining on private properties because of a typescript bug (https://github.com/microsoft/TypeScript/issues/42734) - // We still allow in computed properties - 'foo && foo.#bar;', - '!foo || !foo.#bar;', + // Don't report if the initial type can be falsy but not undefined + ` +declare const foo: { bar: 1 } | false; +foo != null && foo.bar; + `, + ` +declare const foo: { bar: 1 } | 0; +foo != null && foo.bar; + `, + 'foo.bar != null && foo.bar?.() != null && foo.bar?.().baz;', + 'foo !== null && foo.bar;', + 'foo.bar !== null && foo.bar?.() !== null && foo.bar?.().baz;', + 'foo != undefined && foo.bar;', + 'foo.bar != undefined && foo.bar?.() != undefined && foo.bar?.().baz;', + 'foo !== undefined && foo.bar;', + 'foo.bar !== undefined && foo.bar?.() !== undefined && foo.bar?.().baz;', + ` + declare const a: null; + a && a.toString(); + `, + ` + declare const a: bigint | null; + a && a.toString(); + `, + ` + declare const a: bigint | undefined; + a && a.toString(); + `, + ` + declare const a: number | null; + a && a.toString(); + `, + ` + declare const a: number | undefined; + a && a.toString(); + `, + ` + declare const a: string | null; + a && a.toString(); + `, + ` + declare const a: string | undefined; + a && a.toString(); + `, + ` + declare const a: ?(0 | 'abc'); + a && a.length; + `, + ` + declare const a: false | 'abc'; + a && a.length; + `, + ` + declare const a: '' | 'abc'; + a && a.length; + `, + ` + declare const a: { b: false | { c: 'abc' } }; + a.b && a.b.c; + `, + ` + declare const a: { b: 0 | { c: 'abc' } }; + a.b && a.b.c; + `, ], invalid: [ - ...BaseCases.all(), + { + code: 'foo && foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, + ], + }, + ], + }, + { + code: '!foo || !foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: '!foo?.bar;' }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo.bar?.baz;' }, + ], + }, + ], + }, + { + code: 'foo && foo();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo?.();' }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo.bar?.();' }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.baz.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz && foo.bar.baz && foo.bar.baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo[bar] && foo[bar].baz && foo[bar].baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.[bar]?.baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo[bar].baz && foo[bar].baz.buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.[bar].baz?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo[bar.baz] && foo[bar.baz].buzz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.[bar.baz]?.buzz;', + }, + ], + }, + ], + }, + { + code: 'foo[this.bar] && foo[this.bar].baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo[this.bar]?.baz;', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.buzz();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.buzz?.();', + }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz && foo.bar.baz.buzz && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.baz?.buzz?.();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz.buzz();', + }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.baz.buzz();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz.buzz && foo.bar.baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz.buzz?.();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar() && foo.bar().baz && foo.bar().baz.buzz && foo.bar().baz.buzz();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar()?.baz?.buzz?.();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz]();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.[buzz]();', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz[buzz] && foo.bar.baz[buzz]();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.[buzz]?.();', + }, + ], + }, + ], + }, + { + code: 'foo && foo?.bar && foo?.bar.baz && foo?.bar.baz[buzz] && foo?.bar.baz[buzz]();', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar?.baz?.[buzz]?.();', + }, + ], + }, + ], + }, + { + code: 'foo && foo?.bar.baz && foo?.bar.baz[buzz];', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar.baz?.[buzz];', + }, + ], + }, + ], + }, + { + code: 'foo && foo?.() && foo?.().bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo?.()?.bar;' }, + ], + }, + ], + }, + { + code: 'foo.bar && foo.bar?.() && foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo.bar?.()?.baz;' }, + ], + }, + ], + }, // it should ignore whitespace in the expressions - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/\./g, '. '), - })), - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/\./g, '.\n'), - })), + { + code: noFormat`foo && foo . bar;`, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, + ], + }, + ], + }, + { + code: noFormat`foo . bar && foo . bar ?. () && foo . bar ?. ().baz;`, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz;', + }, + ], + }, + ], + }, // it should ignore parts of the expression that aren't part of the expression chain - ...BaseCases.all().map(c => ({ - ...c, - code: `${c.code} && bing`, + { + code: noFormat`foo && foo.bar && bing;`, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { messageId: 'optionalChainSuggest', output: 'foo?.bar && bing;' }, + ], + }, + ], + }, + { + code: noFormat`foo.bar && foo.bar?.() && foo.bar?.().baz && bing;`, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz && bing;', + }, + ], + }, + ], + }, + { + code: 'foo && foo.bar && bing.bong;', errors: [ { - ...c.errors[0], + messageId: 'preferOptionalChain', suggestions: [ { - ...c.errors[0].suggestions![0], - output: `${c.errors[0].suggestions![0].output} && bing`, + messageId: 'optionalChainSuggest', + output: 'foo?.bar && bing.bong;', }, ], }, ], - })), - ...BaseCases.all().map(c => ({ - ...c, - code: `${c.code} && bing.bong`, + }, + { + code: 'foo.bar && foo.bar?.() && foo.bar?.().baz && bing.bong;', errors: [ { - ...c.errors[0], + messageId: 'preferOptionalChain', suggestions: [ { - ...c.errors[0].suggestions![0], - output: `${c.errors[0].suggestions![0].output} && bing.bong`, + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz && bing.bong;', }, ], }, ], - })), - // strict nullish equality checks x !== null && x.y !== null - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/&&/g, '!== null &&'), - })), - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/&&/g, '!= null &&'), - })), - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/&&/g, '!== undefined &&'), - })), - ...BaseCases.all().map(c => ({ - ...c, - code: c.code.replace(/&&/g, '!= undefined &&'), - })), - - // replace && with ||: foo && foo.bar -> !foo || !foo.bar - ...BaseCases.select('canReplaceAndWithOr', true) - .all() - .map(c => ({ - ...c, - code: c.code.replace(/(^|\s)foo/g, '$1!foo').replace(/&&/g, '||'), - errors: [ - { - ...c.errors[0], - suggestions: [ - { - ...c.errors[0].suggestions![0], - output: `!${c.errors[0].suggestions![0].output}`, - }, - ], - }, - ], - })), - - // two errors + }, + // loose and strict null equality checks: x !== null && x.y !== null + { + code: 'foo != null && foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo.bar != null && foo.bar?.() != null && foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo !== null && foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo.bar !== null && foo.bar?.() !== null && foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + // loose and strict undefined equality checks: x !== undefined && x.y !== undefined + { + code: 'foo != undefined && foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo.bar != undefined && foo.bar?.() != undefined && foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo !== undefined && foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo?.bar;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: 'foo.bar !== undefined && foo.bar?.() !== undefined && foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'foo.bar?.()?.baz;', + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + // other looseFalsiness cases + { + code: ` + declare const a: 0 | 'abc'; + a && a.length; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + declare const a: 0 | 'abc'; + a?.length; + `, + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: ` + declare const a: false | 'abc'; + a && a.length; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + declare const a: false | 'abc'; + a?.length; + `, + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: ` + declare const a: '' | 'abc'; + a && a.length; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + declare const a: '' | 'abc'; + a?.length; + `, + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: ` + declare const a: { b: false | { c: 'abc' } }; + a.b && a.b.c; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + declare const a: { b: false | { c: 'abc' } }; + a.b?.c; + `, + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + { + code: ` + declare const a: { b: 0 | { c: 'abc' } }; + a.b && a.b.c; + `, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + declare const a: { b: 0 | { c: 'abc' } }; + a.b?.c; + `, + }, + ], + }, + ], + options: [{ looseFalsiness: true }], + }, + // two errors { code: noFormat`foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -190,7 +857,6 @@ ruleTester.run('prefer-optional-chain', rule, { // case with inconsistent checks { code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -205,7 +871,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: noFormat`foo.bar && foo.bar.baz != null && foo.bar.baz.qux !== undefined && foo.bar.baz.qux.buzz;`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -221,7 +886,6 @@ ruleTester.run('prefer-optional-chain', rule, { // ensure essential whitespace isn't removed { code: 'foo && foo.bar(baz => );', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -233,6 +897,7 @@ ruleTester.run('prefer-optional-chain', rule, { ], }, ], + filename: 'react.tsx', parserOptions: { ecmaFeatures: { jsx: true, @@ -241,7 +906,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: 'foo && foo.bar(baz => typeof baz);', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -256,7 +920,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: noFormat`foo && foo["some long string"] && foo["some long string"].baz`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -271,7 +934,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: noFormat`foo && foo[\`some long string\`] && foo[\`some long string\`].baz`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -286,7 +948,6 @@ ruleTester.run('prefer-optional-chain', rule, { }, { code: "foo && foo['some long string'] && foo['some long string'].baz;", - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -306,7 +967,6 @@ foo && foo.bar(/* comment */a, // comment2 b, ); `, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -326,7 +986,6 @@ foo?.bar(/* comment */a, // ensure binary expressions that are the last expression do not get removed { code: 'foo && foo.bar != null;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -341,7 +1000,6 @@ foo?.bar(/* comment */a, }, { code: 'foo && foo.bar != undefined;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -356,7 +1014,6 @@ foo?.bar(/* comment */a, }, { code: 'foo && foo.bar != null && baz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -372,7 +1029,6 @@ foo?.bar(/* comment */a, // case with this keyword at the start of expression { code: 'this.bar && this.bar.baz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -388,7 +1044,6 @@ foo?.bar(/* comment */a, // other weird cases { code: 'foo && foo?.();', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -403,7 +1058,6 @@ foo?.bar(/* comment */a, }, { code: 'foo.bar && foo.bar?.();', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -419,7 +1073,6 @@ foo?.bar(/* comment */a, // using suggestion instead of autofix { code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -436,7 +1089,6 @@ foo?.bar(/* comment */a, }, { code: 'foo && foo.bar(baz => );', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -450,6 +1102,7 @@ foo?.bar(/* comment */a, ], }, ], + filename: 'react.tsx', parserOptions: { ecmaFeatures: { jsx: true, @@ -1096,10 +1749,37 @@ foo?.bar(/* comment */a, }, ], }, + { + code: '!foo || !foo.bar;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo?.bar;', + }, + ], + }, + ], + }, + { + code: '!foo.bar || !foo.bar?.() || !foo.bar?.().baz;', + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo.bar?.()?.baz;', + }, + ], + }, + ], + }, // case with this keyword at the start of expression { code: '!this.bar || !this.bar.baz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1114,7 +1794,6 @@ foo?.bar(/* comment */a, }, { code: '!a.b || !a.b();', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1129,7 +1808,6 @@ foo?.bar(/* comment */a, }, { code: '!foo.bar || !foo.bar.baz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1144,7 +1822,6 @@ foo?.bar(/* comment */a, }, { code: '!foo[bar] || !foo[bar]?.[baz];', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1159,7 +1836,6 @@ foo?.bar(/* comment */a, }, { code: '!foo || !foo?.bar.baz;', - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1175,7 +1851,6 @@ foo?.bar(/* comment */a, // two errors { code: noFormat`(!foo || !foo.bar || !foo.bar.baz) && (!baz || !baz.bar || !baz.bar.foo);`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1205,7 +1880,6 @@ foo?.bar(/* comment */a, } } `, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1226,7 +1900,6 @@ foo?.bar(/* comment */a, }, { code: noFormat`import.meta && import.meta?.baz;`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1241,7 +1914,6 @@ foo?.bar(/* comment */a, }, { code: noFormat`!import.meta || !import.meta?.baz;`, - output: null, errors: [ { messageId: 'preferOptionalChain', @@ -1254,9 +1926,9 @@ foo?.bar(/* comment */a, }, ], }, + { code: noFormat`import.meta && import.meta?.() && import.meta?.().baz;`, - output: null, errors: [ { messageId: 'preferOptionalChain', diff --git a/packages/type-utils/src/index.ts b/packages/type-utils/src/index.ts index dde032e1770c..094c7a1054ec 100644 --- a/packages/type-utils/src/index.ts +++ b/packages/type-utils/src/index.ts @@ -6,6 +6,7 @@ export * from './getSourceFileOfNode'; export * from './getTokenAtPosition'; export * from './getTypeArguments'; export * from './getTypeName'; +export * from './isTypeIntrinsic'; export * from './isTypeReadonly'; export * from './isUnsafeAssignment'; export * from './predicates'; diff --git a/packages/type-utils/src/isTypeIntrinsic.ts b/packages/type-utils/src/isTypeIntrinsic.ts new file mode 100644 index 000000000000..923b663fef40 --- /dev/null +++ b/packages/type-utils/src/isTypeIntrinsic.ts @@ -0,0 +1,12 @@ +import type * as ts from 'typescript'; + +export interface IntrinsicType extends ts.Type { + intrinsicName: string; +} + +export function isTypeIntrinsic( + type: ts.Type, + intrinsicName: string, +): type is IntrinsicType { + return type.intrinsicName === intrinsicName; +} pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy