Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add no-unnecessary-boolean-literal-compare (#242)
Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
- Loading branch information
1 parent
b835ec2
commit 6bebb1d
Showing
8 changed files
with
372 additions
and
61 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
packages/eslint-plugin/docs/rules/no-unnecessary-boolean-literal-compare.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Flags unnecessary equality comparisons against boolean literals (`no-unnecessary-boolean-literal-compare`) | ||
|
||
Comparing boolean values to boolean literals is unnecessary, those comparisons result in the same booleans. Using the boolean values directly, or via a unary negation (`!value`), is more concise and clearer. | ||
|
||
## Rule Details | ||
|
||
This rule ensures that you do not include unnecessary comparisons with boolean literals. | ||
A comparison is considered unnecessary if it checks a boolean literal against any variable with just the `boolean` type. | ||
A comparison is **_not_** considered unnecessary if the type is a union of booleans (`string | boolean`, `someObject | boolean`). | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
declare const someCondition: boolean; | ||
if (someCondition === true) { | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule | ||
|
||
```ts | ||
declare const someCondition: boolean; | ||
if (someCondition) { | ||
} | ||
|
||
declare const someObjectBoolean: boolean | Record<string, unknown>; | ||
if (someObjectBoolean === true) { | ||
} | ||
|
||
declare const someStringBoolean: boolean | string; | ||
if (someStringBoolean === true) { | ||
} | ||
|
||
declare const someUndefinedCondition: boolean | undefined; | ||
if (someUndefinedCondition === false) { | ||
} | ||
``` | ||
|
||
## Related to | ||
|
||
- TSLint: [no-boolean-literal-compare](https://palantir.github.io/tslint/rules/no-boolean-literal-compare) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { | ||
AST_NODE_TYPES, | ||
TSESTree, | ||
} from '@typescript-eslint/experimental-utils'; | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
import * as util from '../util'; | ||
|
||
type MessageIds = 'direct' | 'negated'; | ||
|
||
interface BooleanComparison { | ||
expression: TSESTree.Expression; | ||
forTruthy: boolean; | ||
negated: boolean; | ||
range: [number, number]; | ||
} | ||
|
||
export default util.createRule<[], MessageIds>({ | ||
name: 'no-unnecessary-boolean-literal-compare', | ||
meta: { | ||
docs: { | ||
description: | ||
'Flags unnecessary equality comparisons against boolean literals', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
requiresTypeChecking: true, | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
direct: | ||
'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.', | ||
}, | ||
schema: [], | ||
type: 'suggestion', | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const parserServices = util.getParserServices(context); | ||
const checker = parserServices.program.getTypeChecker(); | ||
|
||
function getBooleanComparison( | ||
node: TSESTree.BinaryExpression, | ||
): BooleanComparison | undefined { | ||
const comparison = deconstructComparison(node); | ||
if (!comparison) { | ||
return undefined; | ||
} | ||
|
||
const expressionType = checker.getTypeAtLocation( | ||
parserServices.esTreeNodeToTSNodeMap.get(comparison.expression), | ||
); | ||
|
||
if ( | ||
!tsutils.isTypeFlagSet( | ||
expressionType, | ||
ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral, | ||
) | ||
) { | ||
return undefined; | ||
} | ||
|
||
return comparison; | ||
} | ||
|
||
function deconstructComparison( | ||
node: TSESTree.BinaryExpression, | ||
): BooleanComparison | undefined { | ||
const comparisonType = util.getEqualsKind(node.operator); | ||
if (!comparisonType) { | ||
return undefined; | ||
} | ||
|
||
for (const [against, expression] of [ | ||
[node.right, node.left], | ||
[node.left, node.right], | ||
]) { | ||
if ( | ||
against.type !== AST_NODE_TYPES.Literal || | ||
typeof against.value !== 'boolean' | ||
) { | ||
continue; | ||
} | ||
|
||
const { value } = against; | ||
const negated = node.operator.startsWith('!'); | ||
|
||
return { | ||
forTruthy: value ? !negated : negated, | ||
expression, | ||
negated, | ||
range: | ||
expression.range[0] < against.range[0] | ||
? [expression.range[1], against.range[1]] | ||
: [against.range[1], expression.range[1]], | ||
}; | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
return { | ||
BinaryExpression(node): void { | ||
const comparison = getBooleanComparison(node); | ||
|
||
if (comparison) { | ||
context.report({ | ||
fix: function*(fixer) { | ||
yield fixer.removeRange(comparison.range); | ||
|
||
if (!comparison.forTruthy) { | ||
yield fixer.insertTextBefore(node, '!'); | ||
} | ||
}, | ||
messageId: comparison.negated ? 'negated' : 'direct', | ||
node, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
packages/eslint-plugin/tests/rules/no-unnecessary-boolean-literal-compare.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import rule from '../../src/rules/no-unnecessary-boolean-literal-compare'; | ||
import { RuleTester, getFixturesRootDir } from '../RuleTester'; | ||
|
||
const rootDir = getFixturesRootDir(); | ||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaVersion: 2015, | ||
tsconfigRootDir: rootDir, | ||
project: './tsconfig.json', | ||
}, | ||
}); | ||
|
||
ruleTester.run('boolean-literal-compare', rule, { | ||
valid: [ | ||
` | ||
declare const varAny: any; | ||
varAny === true; | ||
`, | ||
` | ||
declare const varAny: any; | ||
varAny == false; | ||
`, | ||
` | ||
declare const varString: string; | ||
varString === false; | ||
`, | ||
` | ||
declare const varString: string; | ||
varString === true; | ||
`, | ||
` | ||
declare const varObject: {}; | ||
varObject === true; | ||
`, | ||
` | ||
declare const varObject: {}; | ||
varObject == false; | ||
`, | ||
` | ||
declare const varBooleanOrString: boolean | undefined; | ||
varBooleanOrString === false; | ||
`, | ||
` | ||
declare const varBooleanOrString: boolean | undefined; | ||
varBooleanOrString == true; | ||
`, | ||
` | ||
declare const varBooleanOrUndefined: boolean | undefined; | ||
varBooleanOrUndefined === true; | ||
`, | ||
`'false' === true;`, | ||
`'true' === false;`, | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: `true === true`, | ||
errors: [ | ||
{ | ||
messageId: 'direct', | ||
}, | ||
], | ||
output: `true`, | ||
}, | ||
{ | ||
code: `false !== true`, | ||
errors: [ | ||
{ | ||
messageId: 'negated', | ||
}, | ||
], | ||
output: `!false`, | ||
}, | ||
{ | ||
code: ` | ||
declare const varBoolean: boolean; | ||
if (varBoolean !== false) { } | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'negated', | ||
}, | ||
], | ||
output: ` | ||
declare const varBoolean: boolean; | ||
if (varBoolean) { } | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
declare const varTrue: true; | ||
if (varTrue !== true) { } | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'negated', | ||
}, | ||
], | ||
output: ` | ||
declare const varTrue: true; | ||
if (!varTrue) { } | ||
`, | ||
}, | ||
], | ||
}); |