fix(language-service): Suggest ? and ! operator on nullable receiver (#35200)

Under strict mode, the language service fails to typecheck nullable
symbols that have already been verified to be non-null.

This generates incorrect (false positive) and confusing diagnostics
for users.

To work around this issue in the short term, this commit changes the
diagnostic message from an error to a suggestion, and prompts users to
use the safe navigation operator (?) or non-null assertion operator (!).

For example, instead of

```typescript
{{ optional && optional.toString() }}
```

the following is cleaner:

```typescript
{{ optional?.toString() }}
{{ optional!.toString() }}
```

Note that with this change, users who legitimately make a typo in their
code will no longer see an error. I think this is acceptable, since
false positive is worse than false negative. However, if users follow
the suggestion, add ? or ! to their code, then the error will be surfaced.
This seems a reasonable trade-off.

References:

1. Safe navigation operator (?)
   https://angular.io/guide/template-syntax#the-safe-navigation-operator----and-null-property-paths
2. Non-null assertion operator (!)
   https://angular.io/guide/template-syntax#the-non-null-assertion-operator---

PR closes https://github.com/angular/angular/pull/35070
PR closes https://github.com/angular/vscode-ng-language-service/issues/589

PR Close #35200
This commit is contained in:
Keen Yee Liau
2020-02-06 14:49:49 -08:00
committed by Kara Erickson
parent a92d97cda7
commit 81241af7ac
4 changed files with 98 additions and 33 deletions

View File

@ -144,7 +144,9 @@ describe('expression diagnostics', () => {
`,
'Identifier \'nume\' is not defined'));
it('should reject access to potentially undefined field',
() => reject(`<div>{{maybe_person.name.first}}`, 'The expression might be null'));
() => reject(
`<div>{{maybe_person.name.first}}`,
`'maybe_person' is possibly undefined. Consider using the safe navigation operator (maybe_person?.name) or non-null assertion operator (maybe_person!.name).`));
it('should accept a safe accss to an undefined field',
() => accept(`<div>{{maybe_person?.name.first}}</div>`));
it('should accept a type assert to an undefined field',
@ -183,7 +185,8 @@ describe('expression diagnostics', () => {
() => accept('<div (click)="click($event)">{{person.name.first}}</div>'));
it('should reject a misspelled event handler',
() => reject(
'<div (click)="clack($event)">{{person.name.first}}</div>', 'Unknown method \'clack\''));
'<div (click)="clack($event)">{{person.name.first}}</div>',
`Identifier 'clack' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`));
it('should reject an uncalled event handler',
() => reject(
'<div (click)="click">{{person.name.first}}</div>', 'Unexpected callable expression'));