Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eef257b

Browse files
authored
fix(eslint-plugin): [prefer-optional-chain] only look at left operand for requireNullish (typescript-eslint#8559)
* ignore right * invert check * invert * add failing case * move operator check * WIP * WIP * pull check out * lint * add tests * add output * quotes * finish tests * remove last child * add a few more tests * remove skipvalidation * update snapshots
1 parent 9e0d9f5 commit eef257b

File tree

7 files changed

+189
-30
lines changed

7 files changed

+189
-30
lines changed

packages/eslint-plugin/docs/rules/prefer-optional-chain.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ When this option is `true` the rule will skip operands that are not typed with `
265265

266266
<TabItem value="❌ Incorrect for `requireNullish: true`">
267267

268-
```ts option='{ "requireNullish": true }' skipValidation
268+
```ts option='{ "requireNullish": true }'
269269
declare const thing1: string | null;
270270
thing1 && thing1.toString();
271271
```

packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
NullThrowsReasons,
2121
OperatorPrecedence,
2222
} from '../../util';
23+
import { checkNullishAndReport } from './checkNullishAndReport';
2324
import { compareNodes, NodeComparisonResult } from './compareNodes';
2425
import type { ValidOperand } from './gatherLogicalOperands';
2526
import { NullishComparisonType } from './gatherLogicalOperands';
@@ -493,20 +494,26 @@ export function analyzeChain(
493494
): void => {
494495
if (subChain.length > 1) {
495496
const subChainFlat = subChain.flat();
496-
context.report({
497-
messageId: 'preferOptionalChain',
498-
loc: {
499-
start: subChainFlat[0].node.loc.start,
500-
end: subChainFlat[subChainFlat.length - 1].node.loc.end,
497+
checkNullishAndReport(
498+
context,
499+
parserServices,
500+
options,
501+
subChainFlat.slice(0, -1).map(({ node }) => node),
502+
{
503+
messageId: 'preferOptionalChain',
504+
loc: {
505+
start: subChainFlat[0].node.loc.start,
506+
end: subChainFlat[subChainFlat.length - 1].node.loc.end,
507+
},
508+
...getFixer(
509+
context.sourceCode,
510+
parserServices,
511+
operator,
512+
options,
513+
subChainFlat,
514+
),
501515
},
502-
...getFixer(
503-
context.sourceCode,
504-
parserServices,
505-
operator,
506-
options,
507-
subChainFlat,
508-
),
509-
});
516+
);
510517
}
511518

512519
// we've reached the end of a chain of logical expressions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { isTypeFlagSet } from '@typescript-eslint/type-utils';
2+
import type {
3+
ParserServicesWithTypeInformation,
4+
TSESTree,
5+
} from '@typescript-eslint/utils';
6+
import type {
7+
ReportDescriptor,
8+
RuleContext,
9+
} from '@typescript-eslint/utils/ts-eslint';
10+
import { unionTypeParts } from 'ts-api-utils';
11+
import * as ts from 'typescript';
12+
13+
import type {
14+
PreferOptionalChainMessageIds,
15+
PreferOptionalChainOptions,
16+
} from './PreferOptionalChainOptions';
17+
18+
export function checkNullishAndReport(
19+
context: RuleContext<
20+
PreferOptionalChainMessageIds,
21+
[PreferOptionalChainOptions]
22+
>,
23+
parserServices: ParserServicesWithTypeInformation,
24+
{ requireNullish }: PreferOptionalChainOptions,
25+
maybeNullishNodes: TSESTree.Expression[],
26+
descriptor: ReportDescriptor<PreferOptionalChainMessageIds>,
27+
): void {
28+
if (
29+
!requireNullish ||
30+
maybeNullishNodes.some(node =>
31+
unionTypeParts(parserServices.getTypeAtLocation(node)).some(t =>
32+
isTypeFlagSet(t, ts.TypeFlags.Null | ts.TypeFlags.Undefined),
33+
),
34+
)
35+
) {
36+
context.report(descriptor);
37+
}
38+
}

packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ function isValidFalseBooleanCheckType(
9090
}
9191
}
9292

93-
if (options.requireNullish === true) {
94-
return types.some(t => isTypeFlagSet(t, NULLISH_FLAGS));
95-
}
96-
9793
let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object;
9894
if (options.checkAny === true) {
9995
allowedFlags |= ts.TypeFlags.Any;

packages/eslint-plugin/src/rules/prefer-optional-chain.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
OperatorPrecedence,
1111
} from '../util';
1212
import { analyzeChain } from './prefer-optional-chain-utils/analyzeChain';
13+
import { checkNullishAndReport } from './prefer-optional-chain-utils/checkNullishAndReport';
1314
import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands';
1415
import {
1516
gatherLogicalOperands,
@@ -141,9 +142,9 @@ export default createRule<
141142

142143
return leftPrecedence < OperatorPrecedence.LeftHandSide;
143144
}
144-
context.report({
145-
node: parentNode,
145+
checkNullishAndReport(context, parserServices, options, [leftNode], {
146146
messageId: 'preferOptionalChain',
147+
node: parentNode,
147148
suggest: [
148149
{
149150
messageId: 'optionalChainSuggest',

packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-optional-chain.shot

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -815,22 +815,64 @@ describe('hand-crafted cases', () => {
815815
declare const x: string;
816816
x && x.length;
817817
`,
818-
options: [
819-
{
820-
requireNullish: true,
821-
},
822-
],
818+
options: [{ requireNullish: true }],
819+
},
820+
{
821+
code: `
822+
declare const foo: string;
823+
foo && foo.toString();
824+
`,
825+
options: [{ requireNullish: true }],
823826
},
824827
{
825828
code: `
826829
declare const x: string | number | boolean | object;
827830
x && x.toString();
828831
`,
829-
options: [
830-
{
831-
requireNullish: true,
832-
},
833-
],
832+
options: [{ requireNullish: true }],
833+
},
834+
{
835+
code: `
836+
declare const foo: { bar: string };
837+
foo && foo.bar && foo.bar.toString();
838+
`,
839+
options: [{ requireNullish: true }],
840+
},
841+
{
842+
code: `
843+
declare const foo: string;
844+
foo && foo.toString() && foo.toString();
845+
`,
846+
options: [{ requireNullish: true }],
847+
},
848+
{
849+
code: `
850+
declare const foo: { bar: string };
851+
foo && foo.bar && foo.bar.toString() && foo.bar.toString();
852+
`,
853+
options: [{ requireNullish: true }],
854+
},
855+
{
856+
code: `
857+
declare const foo1: { bar: string | null };
858+
foo1 && foo1.bar;
859+
`,
860+
options: [{ requireNullish: true }],
861+
},
862+
{
863+
code: `
864+
declare const foo: string;
865+
(foo || {}).toString();
866+
`,
867+
options: [{ requireNullish: true }],
868+
},
869+
870+
{
871+
code: `
872+
declare const foo: string | null;
873+
(foo || 'a' || {}).toString();
874+
`,
875+
options: [{ requireNullish: true }],
834876
},
835877
{
836878
code: `
@@ -1898,6 +1940,80 @@ describe('hand-crafted cases', () => {
18981940
],
18991941
},
19001942

1943+
// requireNullish
1944+
{
1945+
code: `
1946+
declare const thing1: string | null;
1947+
thing1 && thing1.toString();
1948+
`,
1949+
options: [{ requireNullish: true }],
1950+
errors: [{ messageId: 'preferOptionalChain' }],
1951+
},
1952+
{
1953+
code: `
1954+
declare const thing1: string | null;
1955+
thing1 && thing1.toString() && true;
1956+
`,
1957+
options: [{ requireNullish: true }],
1958+
errors: [{ messageId: 'preferOptionalChain' }],
1959+
},
1960+
{
1961+
code: `
1962+
declare const foo: string | null;
1963+
foo && foo.toString() && foo.toString();
1964+
`,
1965+
options: [{ requireNullish: true }],
1966+
errors: [{ messageId: 'preferOptionalChain' }],
1967+
},
1968+
{
1969+
code: `
1970+
declare const foo: { bar: string | null | undefined } | null | undefined;
1971+
foo && foo.bar && foo.bar.toString();
1972+
`,
1973+
output: `
1974+
declare const foo: { bar: string | null | undefined } | null | undefined;
1975+
foo?.bar?.toString();
1976+
`,
1977+
options: [{ requireNullish: true }],
1978+
errors: [{ messageId: 'preferOptionalChain' }],
1979+
},
1980+
{
1981+
code: `
1982+
declare const foo: { bar: string | null | undefined } | null | undefined;
1983+
foo && foo.bar && foo.bar.toString() && foo.bar.toString();
1984+
`,
1985+
output: `
1986+
declare const foo: { bar: string | null | undefined } | null | undefined;
1987+
foo?.bar?.toString() && foo.bar.toString();
1988+
`,
1989+
options: [{ requireNullish: true }],
1990+
errors: [{ messageId: 'preferOptionalChain' }],
1991+
},
1992+
{
1993+
code: `
1994+
declare const foo: string | null;
1995+
(foo || {}).toString();
1996+
`,
1997+
options: [{ requireNullish: true }],
1998+
errors: [{ messageId: 'preferOptionalChain' }],
1999+
},
2000+
{
2001+
code: `
2002+
declare const foo: string;
2003+
(foo || undefined || {}).toString();
2004+
`,
2005+
options: [{ requireNullish: true }],
2006+
errors: [{ messageId: 'preferOptionalChain' }],
2007+
},
2008+
{
2009+
code: `
2010+
declare const foo: string | null;
2011+
(foo || undefined || {}).toString();
2012+
`,
2013+
options: [{ requireNullish: true }],
2014+
errors: [{ messageId: 'preferOptionalChain' }],
2015+
},
2016+
19012017
// allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing
19022018
{
19032019
code: `

0 commit comments

Comments
 (0)