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

@ -366,22 +366,19 @@ export class AstType implements AstVisitor {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name);
if (!method) {
this.reportDiagnostic(`Unknown method '${ast.name}'`, ast);
return this.anyType;
}
if (!method.type) {
const methodType = this.resolvePropertyRead(receiverType, ast);
if (!methodType) {
this.reportDiagnostic(`Could not find a type for '${ast.name}'`, ast);
return this.anyType;
}
if (!method.type.callable) {
if (this.isAny(methodType)) {
return this.anyType;
}
if (!methodType.callable) {
this.reportDiagnostic(`Member '${ast.name}' is not callable`, ast);
return this.anyType;
}
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
const signature = methodType.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature) {
this.reportDiagnostic(`Unable to resolve signature for call of method ${ast.name}`, ast);
return this.anyType;
@ -393,34 +390,33 @@ export class AstType implements AstVisitor {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a property read is the seelcted member's type.
const member = receiverType.members().get(ast.name);
if (!member) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implicit') {
receiverInfo =
'The component declaration, template variable declarations, and element references do';
} else if (receiverType.nullable) {
return this.reportDiagnostic(`The expression might be null`, ast.receiver);
if (receiverType.name === '$implicit') {
this.reportDiagnostic(
`Identifier '${ast.name}' is not defined. ` +
`The component declaration, template variable declarations, and element references do not contain such a member`,
ast);
} else if (receiverType.nullable && ast.receiver instanceof PropertyRead) {
const receiver = ast.receiver.name;
this.reportDiagnostic(
`'${receiver}' is possibly undefined. Consider using the safe navigation operator (${receiver}?.${ast.name}) ` +
`or non-null assertion operator (${receiver}!.${ast.name}).`,
ast, ts.DiagnosticCategory.Suggestion);
} else {
receiverInfo = `'${receiverInfo}' does`;
this.reportDiagnostic(
`Identifier '${ast.name}' is not defined. '${receiverType.name}' does not contain such a member`,
ast);
}
this.reportDiagnostic(
`Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`,
ast);
return this.anyType;
}
if (!member.public) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implicit') {
receiverInfo = 'the component';
} else {
receiverInfo = `'${receiverInfo}'`;
}
this.reportDiagnostic(
`Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast,
ts.DiagnosticCategory.Warning);
`Identifier '${ast.name}' refers to a private member of ${receiverType.name === '$implicit' ? 'the component' : `
'${receiverType.name}'
`}`,
ast, ts.DiagnosticCategory.Warning);
}
return member.type;
}
@ -430,7 +426,7 @@ export class AstType implements AstVisitor {
}
private isAny(symbol: Symbol): boolean {
return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any ||
return !symbol || this.query.getTypeKind(symbol) === BuiltinType.Any ||
(!!symbol.type && this.isAny(symbol.type));
}
}