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:
parent
06779cfe24
commit
18b1bd4415
|
@ -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;
|
||||||
|
|
|
@ -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, `
|
||||||
|
|
Loading…
Reference in New Issue