fix(language-service): infer $implicit value for ngIf template contexts (#35941)

Today, the language service infers the type of variables bound to the
"ngIf" template context member of an NgIf directive, but does not do the
same for the the "$implicit" context member. This commit adds support
for that.

Fixes https://github.com/angular/vscode-ng-language-service/issues/676

PR Close #35941
This commit is contained in:
ayazhafiz 2020-03-08 13:42:05 -07:00 committed by Matias Niemelä
parent 06779cfe24
commit 18b1bd4415
2 changed files with 49 additions and 3 deletions

View File

@ -133,6 +133,7 @@ function getVariableTypeFromDirectiveContext(
} }
} }
} }
return query.getBuiltinType(BuiltinType.Any); return query.getBuiltinType(BuiltinType.Any);
} }
@ -149,7 +150,7 @@ function refinedVariableType(
value: string, mergedTable: SymbolTable, query: SymbolQuery, value: string, mergedTable: SymbolTable, query: SymbolQuery,
templateElement: EmbeddedTemplateAst): Symbol { templateElement: EmbeddedTemplateAst): Symbol {
if (value === '$implicit') { if (value === '$implicit') {
// Special case the ngFor directive // Special case: ngFor directive
const ngForDirective = templateElement.directives.find(d => { const ngForDirective = templateElement.directives.find(d => {
const name = identifierName(d.directive.type); const name = identifierName(d.directive.type);
return name == 'NgFor' || name == 'NgForOf'; return name == 'NgFor' || name == 'NgForOf';
@ -169,13 +170,20 @@ function refinedVariableType(
} }
} }
// Special case the ngIf directive ( *ngIf="data$ | async as variable" ) if (value === 'ngIf' || value === '$implicit') {
if (value === 'ngIf') {
const ngIfDirective = const ngIfDirective =
templateElement.directives.find(d => identifierName(d.directive.type) === 'NgIf'); templateElement.directives.find(d => identifierName(d.directive.type) === 'NgIf');
if (ngIfDirective) { if (ngIfDirective) {
// Special case: ngIf directive. The NgIf structural directive owns a template context with
// "$implicit" and "ngIf" members. These properties are typed as generics. Until the language
// service uses an Ivy and TypecheckBlock backend, we cannot bind these values to a concrete
// type without manual inference. To get the concrete type, look up the type of the "ngIf"
// import on the NgIf directive bound to the template.
//
// See @angular/common/ng_if.ts for more information.
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf'); const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
if (ngIfBinding) { if (ngIfBinding) {
// Check if there is a known type bound to the ngIf input.
const bindingType = new AstType(mergedTable, query, {}).getType(ngIfBinding.value); const bindingType = new AstType(mergedTable, query, {}).getType(ngIfBinding.value);
if (bindingType) { if (bindingType) {
return bindingType; return bindingType;

View File

@ -144,6 +144,44 @@ describe('diagnostics', () => {
}); });
}); });
describe('diagnostics for ngIf exported values', () => {
it('should infer the type of an implicit value in an NgIf context', () => {
mockHost.override(TEST_TEMPLATE, `
<div *ngIf="title; let titleProxy;">
'titleProxy' is a string
{{~{start-err}titleProxy.notAProperty~{end-err}}}
</div>
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length} = diags[0];
expect(messageText)
.toBe(
`Identifier 'notAProperty' is not defined. 'string' does not contain such a member`);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'err');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
it('should infer the type of an ngIf value in an NgIf context', () => {
mockHost.override(TEST_TEMPLATE, `
<div *ngIf="title as titleProxy">
'titleProxy' is a string
{{~{start-err}titleProxy.notAProperty~{end-err}}}
</div>
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length} = diags[0];
expect(messageText)
.toBe(
`Identifier 'notAProperty' is not defined. 'string' does not contain such a member`);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'err');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
});
describe('diagnostics for invalid indexed type property access', () => { describe('diagnostics for invalid indexed type property access', () => {
it('should work with numeric index signatures (arrays)', () => { it('should work with numeric index signatures (arrays)', () => {
mockHost.override(TEST_TEMPLATE, ` mockHost.override(TEST_TEMPLATE, `