Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add rule no-confusing-non-null-assertion #1941

Merged
merged 24 commits into from Jun 7, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b32cfe3
feat: confusing-non-null-assertion-like-not-equal
zhangciwu Apr 24, 2020
75f6276
feat: confusing-non-null-assertion-like-not-equal
zhangciwu Apr 27, 2020
c3fe364
fix: add to config jsons, README
zhangciwu Apr 27, 2020
e29cd94
fix: doc
zhangciwu Apr 27, 2020
f792eb9
fix: lint
zhangciwu Apr 27, 2020
e6ba472
fix: lint
zhangciwu Apr 27, 2020
f62f7c1
Update packages/eslint-plugin/docs/rules/confusing-non-null-assertion…
zhangciwu May 19, 2020
ebc858d
Update packages/eslint-plugin/src/rules/confusing-non-null-assertion-…
zhangciwu May 19, 2020
a4ade6e
Update packages/eslint-plugin/src/rules/confusing-non-null-assertion-…
zhangciwu May 19, 2020
62f28dd
Update packages/eslint-plugin/src/rules/confusing-non-null-assertion-…
zhangciwu May 19, 2020
37ac63e
fix: rename rule
zhangciwu Jun 1, 2020
8f42979
fix: using BinaryExpression as selector
zhangciwu Jun 4, 2020
8350531
fix: ignore when left hand has parentheses, add tests
zhangciwu Jun 4, 2020
c4079be
fix: clean code
zhangciwu Jun 5, 2020
00042bf
fix: doc
zhangciwu Jun 5, 2020
636a4c4
fix: insert to all rules
zhangciwu Jun 5, 2020
89cba74
fix: doc
zhangciwu Jun 5, 2020
46bc044
fix: prettier
zhangciwu Jun 5, 2020
9a70d7c
Update packages/eslint-plugin/docs/rules/no-confusing-non-null-assert…
zhangciwu Jun 6, 2020
163483d
fix: format
zhangciwu Jun 6, 2020
5e4a85e
fix: eslint fix
zhangciwu Jun 6, 2020
c749e97
fix: capitalization
zhangciwu Jun 6, 2020
e32208f
Update packages/eslint-plugin/tests/rules/no-confusing-non-null-asser…
zhangciwu Jun 6, 2020
88559e5
fix: use entire file ignore of eslint
zhangciwu Jun 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -105,6 +105,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/method-signature-style`](./docs/rules/method-signature-style.md) | Enforces using a particular method signature syntax. | | :wrench: | |
| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: |
| [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Requires that `.toString()` is only called on objects which provide useful information when stringified | | | :thought_balloon: |
| [`@typescript-eslint/no-confusing-non-null-assertion`](./docs/rules/no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | | :wrench: | |
| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | |
| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | |
Expand Down
@@ -0,0 +1,46 @@
# Disallow non-null assertion in locations that may be confusing (`no-confusing-non-null-assertion`)

## Rule Details

Using a non-null assertion (`!`) next to an assign or equals check (`=` or `==` or `===`) creates code that is confusing as it looks similar to a not equals check (`!=` `!==`).

```typescript
a! == b; // a non-null assertions(`!`) and an equals test(`==`)
a !== b; // not equals test(`!==`)
a! === b; // a non-null assertions(`!`) and an triple equals test(`===`)
```

Examples of **incorrect** code for this rule:

```ts
interface Foo {
bar?: string;
num?: number;
}

const foo: Foo = getFoo();
const isEqualsBar = foo.bar! == 'hello';
const isEqualsNum = 1 + foo.num! == 2;
```

Examples of **correct** code for this rule:

<!-- prettier-ignore -->
```ts
interface Foo {
bar?: string;
num?: number;
}

const foo: Foo = getFoo();
const isEqualsBar = foo.bar == 'hello';
const isEqualsNum = (1 + foo.num!) == 2;
```

## When Not To Use It

If you don't care about this confusion, then you will not need this rule.

## Further Reading

- [`Issue: Easy misunderstanding: "! ==="`](https://github.com/microsoft/TypeScript/issues/37837) in [TypeScript repo](https://github.com/microsoft/TypeScript)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -37,6 +37,7 @@ export = {
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -6,6 +6,7 @@ import banTypes from './ban-types';
import braceStyle from './brace-style';
import classLiteralPropertyStyle from './class-literal-property-style';
import commaSpacing from './comma-spacing';
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
import consistentTypeAssertions from './consistent-type-assertions';
import consistentTypeDefinitions from './consistent-type-definitions';
import defaultParamLast from './default-param-last';
Expand Down Expand Up @@ -104,6 +105,7 @@ export default {
'brace-style': braceStyle,
'class-literal-property-style': classLiteralPropertyStyle,
'comma-spacing': commaSpacing,
'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual,
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
'default-param-last': defaultParamLast,
Expand Down
@@ -0,0 +1,94 @@
import {
AST_NODE_TYPES,
AST_TOKEN_TYPES,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

export default util.createRule({
name: 'no-confusing-non-null-assertion',
meta: {
type: 'problem',
docs: {
description:
'Disallow non-null assertion in locations that may be confusing',
category: 'Stylistic Issues',
recommended: false,
},
fixable: 'code',
messages: {
confusingEqual:
'Confusing combinations of non-null assertion and equal test like "a! == b", which looks very similar to not equal "a !== b"',
confusingAssign:
'Confusing combinations of non-null assertion and equal test like "a! = b", which looks very similar to not equal "a != b"',
notNeedInEqualTest: 'Unnecessary non-null assertion (!) in equal test',
notNeedInAssign:
'Unnecessary non-null assertion (!) in assignment left hand',
wrapUpLeft:
'Wrap up left hand to avoid putting non-null assertion "!" and "=" together',
},
schema: [],
},
defaultOptions: [],
create(context) {
const sourceCode = context.getSourceCode();
return {
'BinaryExpression, AssignmentExpression'(
node: TSESTree.BinaryExpression | TSESTree.AssignmentExpression,
): void {
function isLeftHandPrimaryExpression(
node: TSESTree.Expression,
): boolean {
return node.type === AST_NODE_TYPES.TSNonNullExpression;
}

if (
node.operator === '==' ||
node.operator === '===' ||
node.operator === '='
) {
const isAssign = node.operator === '=';
const leftHandFinalToken = sourceCode.getLastToken(node.left);
const tokenAfterLeft = sourceCode.getTokenAfter(node.left);
if (
leftHandFinalToken?.type === AST_TOKEN_TYPES.Punctuator &&
leftHandFinalToken?.value === '!' &&
tokenAfterLeft?.value !== ')'
) {
if (isLeftHandPrimaryExpression(node.left)) {
context.report({
node,
messageId: isAssign ? 'confusingAssign' : 'confusingEqual',
suggest: [
{
messageId: isAssign
? 'notNeedInAssign'
: 'notNeedInEqualTest',
fix: (fixer): TSESLint.RuleFix[] => [
fixer.remove(leftHandFinalToken),
],
},
],
});
} else {
context.report({
node,
messageId: isAssign ? 'confusingAssign' : 'confusingEqual',
suggest: [
{
messageId: 'wrapUpLeft',
fix: (fixer): TSESLint.RuleFix[] => [
fixer.insertTextBefore(node.left, '('),
fixer.insertTextAfter(node.left, ')'),
],
},
],
});
}
}
}
},
};
},
});
@@ -0,0 +1,159 @@
import rule from '../../src/rules/no-confusing-non-null-assertion';
zhangciwu marked this conversation as resolved.
Show resolved Hide resolved
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('no-confusing-non-null-assertion', rule, {
valid: [
//
'a == b!;',
'a = b!;',
'a !== b;',
'a != b;',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
'(a + b!) == c;',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
'(a + b!) = c;',
],
invalid: [
{
code: 'a! == b;',
errors: [
{
messageId: 'confusingEqual',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInEqualTest',
output: 'a == b;',
},
],
},
],
},
{
code: 'a! === b;',
errors: [
{
messageId: 'confusingEqual',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInEqualTest',
output: 'a === b;',
},
],
},
],
},
{
code: 'a + b! == c;',
errors: [
{
messageId: 'confusingEqual',
line: 1,
column: 1,
suggestions: [
{
messageId: 'wrapUpLeft',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: '(a + b!) == c;',
},
],
},
],
},
{
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
code: '(obj = new new OuterObj().InnerObj).Name! == c;',
errors: [
{
messageId: 'confusingEqual',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInEqualTest',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: '(obj = new new OuterObj().InnerObj).Name == c;',
},
],
},
],
},
{
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
code: '(a==b)! ==c;',
errors: [
{
messageId: 'confusingEqual',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInEqualTest',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: '(a==b) ==c;',
},
],
},
],
},
{
code: 'a! = b;',
errors: [
{
messageId: 'confusingAssign',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInAssign',
output: 'a = b;',
},
],
},
],
},
{
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
code: '(obj = new new OuterObj().InnerObj).Name! = c;',
errors: [
{
messageId: 'confusingAssign',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInAssign',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: '(obj = new new OuterObj().InnerObj).Name = c;',
},
],
},
],
},
{
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
code: '(a=b)! =c;',
errors: [
{
messageId: 'confusingAssign',
line: 1,
column: 1,
suggestions: [
{
messageId: 'notNeedInAssign',
// eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting
output: '(a=b) =c;',
},
],
},
],
},
],
});