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

Skip to content

Commit c40023e

Browse files
committed
feat(eslint-plugin): [no-unnecessary-conditional][strict-boolean-expressions] add option to make the rules error on files without strictNullChecks turned on
1 parent df37be2 commit c40023e

File tree

9 files changed

+263
-43
lines changed

9 files changed

+263
-43
lines changed

packages/eslint-plugin/docs/rules/no-unnecessary-condition.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,40 @@ function bar(arg?: string | null) {
6262

6363
## Options
6464

65-
Accepts an object with the following options:
65+
```ts
66+
type Options = {
67+
// if true, the rule will ignore constant loop conditions
68+
allowConstantLoopConditions?: boolean;
69+
// if true, the rule will not error when running with a tsconfig that has strictNullChecks turned **off**
70+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
71+
};
72+
73+
const defaultOptions: Options = {
74+
allowConstantLoopConditions: false,
75+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
76+
};
77+
```
6678

67-
- `allowConstantLoopConditions` (default `false`) - allows constant expressions in loops.
79+
### `allowConstantLoopConditions`
6880

69-
Example of correct code for when `allowConstantLoopConditions` is `true`:
81+
Example of correct code for `{ allowConstantLoopConditions: true }`:
7082

7183
```ts
7284
while (true) {}
7385
for (; true; ) {}
7486
do {} while (true);
7587
```
7688

89+
### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing`
90+
91+
If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.
92+
93+
Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule useless.
94+
95+
You should be using `strictNullChecks` to ensure complete type-safety in your codebase.
96+
97+
If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option.
98+
7799
## When Not To Use It
78100

79101
The main downside to using this rule is the need for type information.

packages/eslint-plugin/docs/rules/strict-boolean-expressions.md

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -79,41 +79,81 @@ const foo = (arg: any) => (Boolean(arg) ? 1 : 0);
7979

8080
## Options
8181

82-
Options may be provided as an object with:
83-
84-
- `allowString` (`true` by default) -
85-
Allows `string` in a boolean context.
86-
This is safe because strings have only one falsy value (`""`).
87-
Set this to `false` if you prefer the explicit `str != ""` or `str.length > 0` style.
88-
89-
- `allowNumber` (`true` by default) -
90-
Allows `number` in a boolean context.
91-
This is safe because numbers have only two falsy values (`0` and `NaN`).
92-
Set this to `false` if you prefer the explicit `num != 0` and `!Number.isNaN(num)` style.
93-
94-
- `allowNullableObject` (`true` by default) -
95-
Allows `object | function | symbol | null | undefined` in a boolean context.
96-
This is safe because objects, functions and symbols don't have falsy values.
97-
Set this to `false` if you prefer the explicit `obj != null` style.
98-
99-
- `allowNullableBoolean` (`false` by default) -
100-
Allows `boolean | null | undefined` in a boolean context.
101-
This is unsafe because nullable booleans can be either `false` or nullish.
102-
Set this to `false` if you want to enforce explicit `bool ?? false` or `bool ?? true` style.
103-
Set this to `true` if you don't mind implicitly treating false the same as a nullish value.
104-
105-
- `allowNullableString` (`false` by default) -
106-
Allows `string | null | undefined` in a boolean context.
107-
This is unsafe because nullable strings can be either an empty string or nullish.
108-
Set this to `true` if you don't mind implicitly treating an empty string the same as a nullish value.
109-
110-
- `allowNullableNumber` (`false` by default) -
111-
Allows `number | null | undefined` in a boolean context.
112-
This is unsafe because nullable numbers can be either a falsy number or nullish.
113-
Set this to `true` if you don't mind implicitly treating zero or NaN the same as a nullish value.
114-
115-
- `allowAny` (`false` by default) -
116-
Allows `any` in a boolean context.
82+
```ts
83+
type Options = {
84+
allowString?: boolean;
85+
allowNumber?: boolean;
86+
allowNullableObject?: boolean;
87+
allowNullableBoolean?: boolean;
88+
allowNullableString?: boolean;
89+
allowNullableNumber?: boolean;
90+
allowAny?: boolean;
91+
};
92+
93+
const defaultOptions: Options = {
94+
allowString: true,
95+
allowNumber: true,
96+
allowNullableObject: true,
97+
allowNullableBoolean: false,
98+
allowNullableString: false,
99+
allowNullableNumber: false,
100+
allowAny: false,
101+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
102+
};
103+
```
104+
105+
### `allowString`
106+
107+
Allows `string` in a boolean context.
108+
This is safe because strings have only one falsy value (`""`).
109+
Set this to `false` if you prefer the explicit `str != ""` or `str.length > 0` style.
110+
111+
### `allowNumber`
112+
113+
Allows `number` in a boolean context.
114+
This is safe because numbers have only two falsy values (`0` and `NaN`).
115+
Set this to `false` if you prefer the explicit `num != 0` and `!Number.isNaN(num)` style.
116+
117+
### `allowNullableObject`
118+
119+
Allows `object | function | symbol | null | undefined` in a boolean context.
120+
This is safe because objects, functions and symbols don't have falsy values.
121+
Set this to `false` if you prefer the explicit `obj != null` style.
122+
123+
### `allowNullableBoolean`
124+
125+
Allows `boolean | null | undefined` in a boolean context.
126+
This is unsafe because nullable booleans can be either `false` or nullish.
127+
Set this to `false` if you want to enforce explicit `bool ?? false` or `bool ?? true` style.
128+
Set this to `true` if you don't mind implicitly treating false the same as a nullish value.
129+
130+
### `allowNullableString`
131+
132+
Allows `string | null | undefined` in a boolean context.
133+
This is unsafe because nullable strings can be either an empty string or nullish.
134+
Set this to `true` if you don't mind implicitly treating an empty string the same as a nullish value.
135+
136+
### `allowNullableNumber`
137+
138+
Allows `number | null | undefined` in a boolean context.
139+
This is unsafe because nullable numbers can be either a falsy number or nullish.
140+
Set this to `true` if you don't mind implicitly treating zero or NaN the same as a nullish value.
141+
142+
### `allowAny`
143+
144+
Allows `any` in a boolean context.
145+
This is unsafe for obvious reasons.
146+
Set this to `true` at your own risk.
147+
148+
### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing`
149+
150+
If this is set to `false`, then the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.
151+
152+
Without `strictNullChecks`, TypeScript essentially erases `undefined` and `null` from the types. This means when this rule inspects the types from a variable, **it will not be able to tell that the variable might be `null` or `undefined`**, which essentially makes this rule a lot less useful.
153+
154+
You should be using `strictNullChecks` to ensure complete type-safety in your codebase.
155+
156+
If for some reason you cannot turn on `strictNullChecks`, but still want to use this rule - you can use this option to allow it - but know that the behavior of this rule is _undefined_ with the compiler option turned off. We will not accept bug reports if you are using this option.
117157

118158
## Related To
119159

packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const isLiteral = (type: ts.Type): boolean =>
6565
export type Options = [
6666
{
6767
allowConstantLoopConditions?: boolean;
68+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
6869
},
6970
];
7071

@@ -78,7 +79,9 @@ export type MessageId =
7879
| 'literalBooleanExpression'
7980
| 'noOverlapBooleanExpression'
8081
| 'never'
81-
| 'neverOptionalChain';
82+
| 'neverOptionalChain'
83+
| 'noStrictNullCheck';
84+
8285
export default createRule<Options, MessageId>({
8386
name: 'no-unnecessary-condition',
8487
meta: {
@@ -97,6 +100,9 @@ export default createRule<Options, MessageId>({
97100
allowConstantLoopConditions: {
98101
type: 'boolean',
99102
},
103+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
104+
type: 'boolean',
105+
},
100106
},
101107
additionalProperties: false,
102108
},
@@ -119,18 +125,46 @@ export default createRule<Options, MessageId>({
119125
'Unnecessary conditional, the types have no overlap',
120126
never: 'Unnecessary conditional, value is `never`',
121127
neverOptionalChain: 'Unnecessary optional chain on a non-nullish value',
128+
noStrictNullCheck:
129+
'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
122130
},
123131
},
124132
defaultOptions: [
125133
{
126134
allowConstantLoopConditions: false,
135+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
127136
},
128137
],
129-
create(context, [{ allowConstantLoopConditions }]) {
138+
create(
139+
context,
140+
[
141+
{
142+
allowConstantLoopConditions,
143+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
144+
},
145+
],
146+
) {
130147
const service = getParserServices(context);
131148
const checker = service.program.getTypeChecker();
132149
const sourceCode = context.getSourceCode();
133150
const compilerOptions = service.program.getCompilerOptions();
151+
const isStrictNullChecks = isStrictCompilerOptionEnabled(
152+
compilerOptions,
153+
'strictNullChecks',
154+
);
155+
156+
if (
157+
!isStrictNullChecks &&
158+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
159+
) {
160+
context.report({
161+
loc: {
162+
start: { line: 0, column: 0 },
163+
end: { line: 0, column: 0 },
164+
},
165+
messageId: 'noStrictNullCheck',
166+
});
167+
}
134168

135169
function getNodeType(node: TSESTree.Expression): ts.Type {
136170
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
@@ -263,7 +297,7 @@ export default createRule<Options, MessageId>({
263297
return;
264298
}
265299
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
266-
if (isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks')) {
300+
if (isStrictNullChecks) {
267301
const UNDEFINED = ts.TypeFlags.Undefined;
268302
const NULL = ts.TypeFlags.Null;
269303
const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {

packages/eslint-plugin/src/rules/strict-boolean-expressions.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type Options = [
1515
allowNullableString?: boolean;
1616
allowNullableNumber?: boolean;
1717
allowAny?: boolean;
18+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
1819
},
1920
];
2021

@@ -28,7 +29,8 @@ export type MessageId =
2829
| 'conditionErrorNumber'
2930
| 'conditionErrorNullableNumber'
3031
| 'conditionErrorObject'
31-
| 'conditionErrorNullableObject';
32+
| 'conditionErrorNullableObject'
33+
| 'noStrictNullCheck';
3234

3335
export default util.createRule<Options, MessageId>({
3436
name: 'strict-boolean-expressions',
@@ -51,6 +53,9 @@ export default util.createRule<Options, MessageId>({
5153
allowNullableString: { type: 'boolean' },
5254
allowNullableNumber: { type: 'boolean' },
5355
allowAny: { type: 'boolean' },
56+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: {
57+
type: 'boolean',
58+
},
5459
},
5560
additionalProperties: false,
5661
},
@@ -86,18 +91,43 @@ export default util.createRule<Options, MessageId>({
8691
conditionErrorNullableObject:
8792
'Unexpected nullable object value in conditional. ' +
8893
'An explicit null check is required.',
94+
noStrictNullCheck:
95+
'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.',
8996
},
9097
},
9198
defaultOptions: [
9299
{
93100
allowString: true,
94101
allowNumber: true,
95102
allowNullableObject: true,
103+
allowNullableBoolean: false,
104+
allowNullableString: false,
105+
allowNullableNumber: false,
106+
allowAny: false,
107+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false,
96108
},
97109
],
98110
create(context, [options]) {
99111
const service = util.getParserServices(context);
100112
const checker = service.program.getTypeChecker();
113+
const compilerOptions = service.program.getCompilerOptions();
114+
const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
115+
compilerOptions,
116+
'strictNullChecks',
117+
);
118+
119+
if (
120+
!isStrictNullChecks &&
121+
options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
122+
) {
123+
context.report({
124+
loc: {
125+
start: { line: 0, column: 0 },
126+
end: { line: 0, column: 0 },
127+
},
128+
messageId: 'noStrictNullCheck',
129+
});
130+
}
101131

102132
const checkedNodes = new Set<TSESTree.Node>();
103133

packages/eslint-plugin/tests/fixtures/unstrict/file.ts

Whitespace-only changes.

packages/eslint-plugin/tests/fixtures/unstrict/react.tsx

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "preserve",
4+
"target": "es5",
5+
"module": "commonjs",
6+
"strict": false,
7+
"esModuleInterop": true,
8+
"lib": ["es2015", "es2017", "esnext"],
9+
"experimentalDecorators": true
10+
},
11+
"include": [
12+
"file.ts",
13+
"react.tsx"
14+
]
15+
}

packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
TestCaseError,
33
InvalidTestCase,
44
} from '@typescript-eslint/experimental-utils/dist/ts-eslint';
5+
import * as path from 'path';
56
import rule, {
67
Options,
78
MessageId,
@@ -445,6 +446,22 @@ declare const key: Key;
445446
446447
foo?.[key]?.trim();
447448
`,
449+
{
450+
code: `
451+
declare const x: string[] | null;
452+
// eslint-disable-next-line
453+
if (x) {
454+
}
455+
`,
456+
options: [
457+
{
458+
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: true,
459+
},
460+
],
461+
parserOptions: {
462+
tsconfigRootDir: path.join(rootPath, 'unstrict'),
463+
},
464+
},
448465
],
449466
invalid: [
450467
// Ensure that it's checking in all the right places
@@ -1367,5 +1384,27 @@ function Foo(outer: Outer, key: Bar): number | undefined {
13671384
},
13681385
],
13691386
},
1387+
{
1388+
code: `
1389+
declare const x: string[] | null;
1390+
if (x) {
1391+
}
1392+
`,
1393+
errors: [
1394+
{
1395+
messageId: 'noStrictNullCheck',
1396+
line: 0,
1397+
column: 1,
1398+
},
1399+
{
1400+
messageId: 'alwaysTruthy',
1401+
line: 3,
1402+
column: 5,
1403+
},
1404+
],
1405+
parserOptions: {
1406+
tsconfigRootDir: path.join(rootPath, 'unstrict'),
1407+
},
1408+
},
13701409
],
13711410
});

0 commit comments

Comments
 (0)