Skip to content

Commit

Permalink
feat(typescript-estree): handle 3.9's non-null assertion changes
Browse files Browse the repository at this point in the history
[TS 3.9 introduced a breaking change for how non-null assertions are handled in optional chains](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#parsing-differences-in-optional-chaining-and-non-null-assertions).

Pre-3.9,  `x?.y!.z` means `(x?.y).z` - i.e. it essentially scrubbed the optionality from the chain
Post-3.9, `x?.y!.z` means `x?.y!.z`  - i.e. it just asserts that the property `y` is non-null, not the result of `x?.y`

This PR:

**updates the AST so it matches this**

Previously `x?.y!.z` produced `MemberExpression > TSNonNullAssertion > OptionalMemberExpression`.
Now it produces `OptionalMemberExpression > TSNonNullAssertion > OptionalMemberExpression`.

Note that
`(x?.y)!.z` still produces `MemberExpression > TSNonNullAssertion > OptionalMemberExpression`.

Same results apply for call expressions.

**updates `no-non-null-asserted-optional-chain` to handle this**

With the above AST change, the only change needed was a simple "is parent optional chain" check.

Both of these changes are gated behind a version check, so they are completely backwards compatible.
  • Loading branch information
bradzacher committed May 18, 2020
1 parent 1bc105a commit 81741e6
Show file tree
Hide file tree
Showing 16 changed files with 9,958 additions and 3,626 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -78,7 +78,7 @@
"ts-jest": "^25.5.1",
"ts-node": "^8.10.1",
"tslint": "^6.1.2",
"typescript": ">=3.2.1 <3.9.0"
"typescript": ">=3.2.1 <4.0.0"
},
"resolutions": {
"typescript": "^3.8.3"
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/package.json
Expand Up @@ -44,6 +44,7 @@
"@typescript-eslint/experimental-utils": "2.33.0",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"devDependencies": {
Expand Down
@@ -1,8 +1,19 @@
import { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
import {
TSESTree,
TSESLint,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import { version } from 'typescript';
import * as semver from 'semver';
import * as util from '../util';

type MessageIds = 'noNonNullOptionalChain' | 'suggestRemovingNonNull';

const is3dot9 = !semver.satisfies(
version,
'< 3.9.0 || < 3.9.1-rc || < 3.9.0-beta',
);

export default util.createRule<[], MessageIds>({
name: 'no-non-null-asserted-optional-chain',
meta: {
Expand All @@ -29,6 +40,24 @@ export default util.createRule<[], MessageIds>({
| TSESTree.OptionalCallExpression
| TSESTree.OptionalMemberExpression,
): void {
if (is3dot9) {
// TS3.9 made a breaking change to how non-null works with optional chains.
// Pre-3.9, `x?.y!.z` means `(x?.y).z` - i.e. it essentially scrubbed the optionality from the chain
// Post-3.9, `x?.y!.z` means `x?.y!.z` - i.e. it just asserts that the property `y` is non-null, not the result of `x?.y`.
// This means that for > 3.9, x?.y!.z is valid!
// NOTE: these cases are still invalid:
// - x?.y.z!
// - (x?.y)!.z
const nnAssertionParent = node.parent?.parent;
if (
nnAssertionParent?.type ===
AST_NODE_TYPES.OptionalMemberExpression ||
nnAssertionParent?.type === AST_NODE_TYPES.OptionalCallExpression
) {
return;
}
}

// selector guarantees this assertion
const parent = node.parent as TSESTree.TSNonNullExpression;
context.report({
Expand Down
Expand Up @@ -13,6 +13,10 @@ ruleTester.run('no-non-null-asserted-optional-chain', rule, {
'foo?.bar();',
'(foo?.bar).baz!;',
'(foo?.bar()).baz!;',
// Valid as of 3.9
'foo?.bar!.baz;',
'foo?.bar!();',
"foo?.['bar']!.baz;",
],
invalid: [
{
Expand Down Expand Up @@ -71,20 +75,6 @@ ruleTester.run('no-non-null-asserted-optional-chain', rule, {
},
],
},
{
code: 'foo?.bar!();',
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: 'foo?.bar();',
},
],
},
],
},
{
code: noFormat`(foo?.bar)!.baz`,
errors: [
Expand All @@ -99,20 +89,6 @@ ruleTester.run('no-non-null-asserted-optional-chain', rule, {
},
],
},
{
code: "foo?.['bar']!.baz;",
errors: [
{
messageId: 'noNonNullOptionalChain',
suggestions: [
{
messageId: 'suggestRemovingNonNull',
output: "foo?.['bar'].baz;",
},
],
},
],
},
{
code: noFormat`(foo?.bar)!().baz`,
errors: [
Expand Down

0 comments on commit 81741e6

Please sign in to comment.