From fabc791d791b26c964a381ead6d03fe26b147dfd Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 26 Jan 2025 02:24:56 +0900 Subject: [PATCH 1/2] feat(eslint-plugin): [no-unnecessary-boolean-literal-compare] enforce strictNullChecks --- ...no-unnecessary-boolean-literal-compare.mdx | 16 ++++++++ .../no-unnecessary-boolean-literal-compare.ts | 31 ++++++++++++++- ...nnecessary-boolean-literal-compare.test.ts | 39 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.mdx index 16462771e768..3562b8d09c91 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.mdx @@ -129,6 +129,22 @@ if (someNullCondition ?? true) { +### `allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing` + +:::danger Deprecated + +This option will be removed in the next major version of typescript-eslint. + +::: + +{/* insert option description */} + +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. + +You should be using `strictNullChecks` to ensure complete type-safety in your codebase. + +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. + ## Fixer | Comparison | Fixer Output | Notes | diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts index dde39ccc223f..d5ed0974ecce 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts @@ -16,12 +16,14 @@ export type MessageIds = | 'comparingNullableToTrueDirect' | 'comparingNullableToTrueNegated' | 'direct' - | 'negated'; + | 'negated' + | 'noStrictNullCheck'; export type Options = [ { allowComparingNullableBooleansToFalse?: boolean; allowComparingNullableBooleansToTrue?: boolean; + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; }, ]; @@ -57,6 +59,8 @@ export default createRule({ 'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.', negated: 'This expression unnecessarily compares a boolean value to a boolean instead of negating it.', + noStrictNullCheck: + 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', }, schema: [ { @@ -73,6 +77,11 @@ export default createRule({ description: 'Whether to allow comparisons between nullable boolean variables and `true`.', }, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + type: 'boolean', + description: + 'Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`.', + }, }, }, ], @@ -81,11 +90,31 @@ export default createRule({ { allowComparingNullableBooleansToFalse: true, allowComparingNullableBooleansToTrue: true, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, }, ], create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); + const compilerOptions = services.program.getCompilerOptions(); + + const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled( + compilerOptions, + 'strictNullChecks', + ); + + if ( + !isStrictNullChecks && + options.allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true + ) { + context.report({ + loc: { + start: { column: 0, line: 0 }, + end: { column: 0, line: 0 }, + }, + messageId: 'noStrictNullCheck', + }); + } function getBooleanComparison( node: TSESTree.BinaryExpression, diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts index 77d395aeb4d1..ef3107357a77 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts @@ -1,4 +1,5 @@ import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; +import * as path from 'node:path'; import rule from '../../src/rules/no-unnecessary-boolean-literal-compare'; import { getFixturesRootDir } from '../RuleTester'; @@ -123,6 +124,24 @@ const extendsUnknown: ( } }; `, + { + code: ` +function test(a?: boolean): boolean { + // eslint-disable-next-line + return a !== false; +} + `, + languageOptions: { + parserOptions: { + tsconfigRootDir: path.join(rootDir, 'unstrict'), + }, + }, + options: [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: true, + }, + ], + }, ], invalid: [ @@ -586,5 +605,25 @@ const extendsUnknown: ( }; `, }, + { + code: ` +function foo(): boolean {} + `, + errors: [ + { + messageId: 'noStrictNullCheck', + }, + ], + languageOptions: { + parserOptions: { + tsconfigRootDir: path.join(rootDir, 'unstrict'), + }, + }, + options: [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + }, + ], + }, ], }); From c1ff73f6e73df2a5ed3e4eba12d621800e5a3010 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 26 Jan 2025 02:58:45 +0900 Subject: [PATCH 2/2] fixup --- packages/eslint-plugin/tests/docs.test.ts | 1 + .../no-unnecessary-boolean-literal-compare.shot | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index 412250206d14..89e499e38e58 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -201,6 +201,7 @@ describe('Validating rule docs', () => { 'switch-exhaustiveness-check', 'switch-exhaustiveness-check', 'unbound-method', + 'no-unnecessary-boolean-literal-compare', ]); it('All rules must have a corresponding rule doc', () => { diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-boolean-literal-compare.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-boolean-literal-compare.shot index faf2bf9e1822..4072b4f40e53 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-boolean-literal-compare.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-boolean-literal-compare.shot @@ -15,6 +15,10 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "allowComparingNullableBooleansToTrue": { "description": "Whether to allow comparisons between nullable boolean variables and \`true\`.", "type": "boolean" + }, + "allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing": { + "description": "Unless this is set to \`true\`, the rule will error on every file whose \`tsconfig.json\` does _not_ have the \`strictNullChecks\` compiler option (or \`strict\`) set to \`true\`.", + "type": "boolean" } }, "type": "object" @@ -30,6 +34,8 @@ type Options = [ allowComparingNullableBooleansToFalse?: boolean; /** Whether to allow comparisons between nullable boolean variables and \`true\`. */ allowComparingNullableBooleansToTrue?: boolean; + /** Unless this is set to \`true\`, the rule will error on every file whose \`tsconfig.json\` does _not_ have the \`strictNullChecks\` compiler option (or \`strict\`) set to \`true\`. */ + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; }, ]; "