Skip to content

Commit

Permalink
feat(eslint-plugin): add no-non-null-asserted-optional-chain (#1469)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Jan 18, 2020
1 parent 9c5b857 commit 498aa24
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 79 deletions.
157 changes: 79 additions & 78 deletions packages/eslint-plugin/README.md

Large diffs are not rendered by default.

@@ -0,0 +1,39 @@
# Disallows using a non-null assertion after an optional chain expression (`no-non-null-asserted-optional-chain`)

## Rule Details

Optional chain expressions are designed to return `undefined` if the optional property is nullish.
Using non-null assertions after an optional chain expression is wrong, and introduces a serious type safety hole into your code.

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

```ts
/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */

foo?.bar!;
foo?.bar!.baz;
foo?.bar()!;
foo?.bar!();
foo?.bar!().baz;
```

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

```ts
/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */

foo?.bar;
(foo?.bar).baz;
foo?.bar();
foo?.bar();
foo?.bar().baz;
```

## When Not To Use It

If you are not using TypeScript 3.7 (or greater), then you will not need to use this rule, as the operator is not supported.

## Further Reading

- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
- [Optional Chaining Proposal](https://github.com/tc39/proposal-optional-chaining/)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -44,6 +44,7 @@
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-parameter-properties": "error",
"@typescript-eslint/no-require-imports": "error",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -38,6 +38,7 @@ import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
import noNamespace from './no-namespace';
import noNonNullAssertion from './no-non-null-assertion';
import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain';
import noParameterProperties from './no-parameter-properties';
import noRequireImports from './no-require-imports';
import noThisAlias from './no-this-alias';
Expand Down Expand Up @@ -120,6 +121,7 @@ export default {
'no-misused-promises': noMisusedPromises,
'no-namespace': noNamespace,
'no-non-null-assertion': noNonNullAssertion,
'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain,
'no-parameter-properties': noParameterProperties,
'no-require-imports': noRequireImports,
'no-this-alias': noThisAlias,
Expand Down
@@ -0,0 +1,52 @@
import { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
import * as util from '../util';

type MessageIds = 'noNonNullOptionalChain' | 'suggestRemovingNonNull';

export default util.createRule<[], MessageIds>({
name: 'no-non-null-asserted-optional-chain',
meta: {
type: 'problem',
docs: {
description:
'Disallows using a non-null assertion after an optional chain expression',
category: 'Possible Errors',
recommended: false,
},
messages: {
noNonNullOptionalChain:
'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.',
suggestRemovingNonNull: 'You should remove the non-null assertion.',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
'TSNonNullExpression > :matches(OptionalMemberExpression, OptionalCallExpression)'(
node:
| TSESTree.OptionalCallExpression
| TSESTree.OptionalMemberExpression,
): void {
// selector guarantees this assertion
const parent = node.parent as TSESTree.TSNonNullExpression;
context.report({
node,
messageId: 'noNonNullOptionalChain',
// use a suggestion instead of a fixer, because this can obviously break type checks
suggest: [
{
messageId: 'suggestRemovingNonNull',
fix(fixer): TSESLint.RuleFix {
return fixer.removeRange([
parent.range[1] - 1,
parent.range[1],
]);
},
},
],
});
},
};
},
});
@@ -0,0 +1,187 @@
import rule from '../../src/rules/no-non-null-asserted-optional-chain';
import { RuleTester } from '../RuleTester';

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

ruleTester.run('no-non-null-asserted-optional-chain', rule, {
valid: [
'foo.bar!',
'foo.bar()!',
'foo?.bar',
'foo?.bar()',
'(foo?.bar).baz!',
'(foo?.bar()).baz!',
],
invalid: [
{
code: 'foo?.bar!',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.bar',
},
],
},
],
},
{
code: 'foo?.["bar"]!',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.["bar"]',
},
],
},
],
},
{
code: 'foo?.bar()!',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.bar()',
},
],
},
],
},
{
code: 'foo.bar?.()!',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo.bar?.()',
},
],
},
],
},
{
code: 'foo?.bar!()',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.bar()',
},
],
},
],
},
{
code: '(foo?.bar)!.baz',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar).baz',
},
],
},
],
},
{
code: 'foo?.["bar"]!.baz',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.["bar"].baz',
},
],
},
],
},
{
code: '(foo?.bar)!().baz',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar)().baz',
},
],
},
],
},
{
code: '(foo?.bar)!',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar)',
},
],
},
],
},
{
code: '(foo?.bar)!()',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar)()',
},
],
},
],
},
{
code: '(foo?.bar!)',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar)',
},
],
},
],
},
{
code: '(foo?.bar!)()',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: '(foo?.bar)()',
},
],
},
],
},
],
});
5 changes: 4 additions & 1 deletion packages/eslint-plugin/tools/generate-configs.ts
Expand Up @@ -19,7 +19,10 @@ interface LinterConfig extends TSESLint.Linter.Config {
}

const RULE_NAME_PREFIX = '@typescript-eslint/';
const MAX_RULE_NAME_LENGTH = 32;
const MAX_RULE_NAME_LENGTH = Object.keys(rules).reduce(
(acc, name) => Math.max(acc, name.length),
0,
);
const DEFAULT_RULE_SETTING = 'warn';
const BASE_RULES_TO_BE_OVERRIDDEN = new Map(
Object.entries(rules)
Expand Down

0 comments on commit 498aa24

Please sign in to comment.