Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(typescript-estree): handle 3.9's non-null assertion changes (#2036)
  • Loading branch information
bradzacher committed May 21, 2020
1 parent a1816c9 commit 06bec63
Show file tree
Hide file tree
Showing 19 changed files with 9,987 additions and 3,659 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.34.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
13 changes: 6 additions & 7 deletions packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts
Expand Up @@ -39,6 +39,12 @@ function foo(x: { a?: () => void }) {
let foo: any = 23;
String(foo); // ERROR: Unsafe call of an any typed value
`,
// TS 3.9 changed this to be safe
`
function foo<T extends any>(x: T) {
x();
}
`,
],
invalid: [
...batchedSingleLineTests({
Expand All @@ -47,7 +53,6 @@ function foo(x: any) { x() }
function foo(x: any) { x?.() }
function foo(x: any) { x.a.b.c.d.e.f.g() }
function foo(x: any) { x.a.b.c.d.e.f.g?.() }
function foo<T extends any>(x: T) { x() }
`,
errors: [
{
Expand All @@ -74,12 +79,6 @@ function foo<T extends any>(x: T) { x() }
column: 24,
endColumn: 39,
},
{
messageId: 'unsafeCall',
line: 6,
column: 37,
endColumn: 38,
},
],
}),
...batchedSingleLineTests({
Expand Down
24 changes: 6 additions & 18 deletions packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts
Expand Up @@ -71,26 +71,14 @@ function foo(): Set<number> {
return { prop: '' } as Foo;
}
`,
// TS 3.9 changed this to be safe
`
function fn<T extends any>(x: T) {
return x;
}
`,
],
invalid: [
{
code: `
function fn<T extends any>(x: T) {
return x;
}
`,
errors: [
{
messageId: 'unsafeReturnAssignment',
data: {
sender: 'any',
receiver: 'T',
},
line: 3,
column: 3,
},
],
},
...batchedSingleLineTests({
code: noFormat`
function foo() { return (1 as any); }
Expand Down
Expand Up @@ -158,14 +158,6 @@ ruleTester.run('restrict-template-expressions', rule, {
const msg = \`arg = \${user.name || 'the user with no name'}\`;
`,
},
{
options: [{ allowAny: true }],
code: `
function test<T extends any>(arg: T) {
return \`arg = \${arg}\`;
}
`,
},
// allowNullable
{
options: [{ allowNullable: true }],
Expand Down Expand Up @@ -344,5 +336,22 @@ ruleTester.run('restrict-template-expressions', rule, {
},
],
},
// TS 3.9 change
{
options: [{ allowAny: true }],
code: `
function test<T extends any>(arg: T) {
return \`arg = \${arg}\`;
}
`,
errors: [
{
messageId: 'invalidType',
data: { type: 'unknown' },
line: 3,
column: 27,
},
],
},
],
});

0 comments on commit 06bec63

Please sign in to comment.