fix(language-service): Resolve template variable in nested ngFor (#33676)
This commit fixes a bug whereby template variables in nested scope are not resolved properly and instead are simply typed as `any`. PR closes https://github.com/angular/vscode-ng-language-service/issues/144 PR Close #33676
This commit is contained in:
parent
942e2ebe44
commit
8b91ea5532
@ -90,52 +90,77 @@ function getDefinitionOf(info: DiagnosticTemplateInfo, ast: TemplateAst): Defini
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVarDeclarations(
|
/**
|
||||||
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
|
* Resolve the specified `variable` from the `directives` list and return the
|
||||||
const result: SymbolDeclaration[] = [];
|
* corresponding symbol. If resolution fails, return the `any` type.
|
||||||
|
* @param variable template variable to resolve
|
||||||
let current = path.tail;
|
* @param directives template context
|
||||||
while (current) {
|
* @param query
|
||||||
if (current instanceof EmbeddedTemplateAst) {
|
*/
|
||||||
for (const variable of current.variables) {
|
function findSymbolForVariableInDirectives(
|
||||||
const name = variable.name;
|
variable: VariableAst, directives: DirectiveAst[], query: SymbolQuery): Symbol {
|
||||||
|
for (const d of directives) {
|
||||||
// Find the first directive with a context.
|
// Get the symbol table for the directive's StaticSymbol
|
||||||
const context =
|
const table = query.getTemplateContext(d.directive.type.reference);
|
||||||
current.directives.map(d => info.query.getTemplateContext(d.directive.type.reference))
|
if (!table) {
|
||||||
.find(c => !!c);
|
continue;
|
||||||
|
}
|
||||||
// Determine the type of the context field referenced by variable.value.
|
const symbol = table.get(variable.value);
|
||||||
let type: Symbol|undefined = undefined;
|
if (symbol) {
|
||||||
if (context) {
|
return symbol;
|
||||||
const value = context.get(variable.value);
|
|
||||||
if (value) {
|
|
||||||
type = value.type !;
|
|
||||||
let kind = info.query.getTypeKind(type);
|
|
||||||
if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) {
|
|
||||||
// The any type is not very useful here. For special cases, such as ngFor, we can do
|
|
||||||
// better.
|
|
||||||
type = refinedVariableType(type, info, current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!type) {
|
|
||||||
type = info.query.getBuiltinType(BuiltinType.Any);
|
|
||||||
}
|
|
||||||
result.push({
|
|
||||||
name,
|
|
||||||
kind: 'variable', type, get definition() { return getDefinitionOf(info, variable); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
current = path.parentOf(current);
|
|
||||||
}
|
}
|
||||||
|
return query.getBuiltinType(BuiltinType.Any);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve all variable declarations in a template by traversing the specified
|
||||||
|
* `path`.
|
||||||
|
* @param info
|
||||||
|
* @param path template AST path
|
||||||
|
*/
|
||||||
|
function getVarDeclarations(
|
||||||
|
info: DiagnosticTemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
|
||||||
|
const results: SymbolDeclaration[] = [];
|
||||||
|
for (let current = path.head; current; current = path.childOf(current)) {
|
||||||
|
if (!(current instanceof EmbeddedTemplateAst)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {directives, variables} = current;
|
||||||
|
for (const variable of variables) {
|
||||||
|
let symbol = findSymbolForVariableInDirectives(variable, directives, info.query);
|
||||||
|
const kind = info.query.getTypeKind(symbol);
|
||||||
|
if (kind === BuiltinType.Any || kind === BuiltinType.Unbound) {
|
||||||
|
// For special cases such as ngFor and ngIf, the any type is not very useful.
|
||||||
|
// We can do better by resolving the binding value.
|
||||||
|
const symbolsInScope = info.query.mergeSymbolTable([
|
||||||
|
info.members,
|
||||||
|
// Since we are traversing the AST path from head to tail, any variables
|
||||||
|
// that have been declared so far are also in scope.
|
||||||
|
info.query.createSymbolTable(results),
|
||||||
|
]);
|
||||||
|
symbol = refinedVariableType(symbolsInScope, info.query, current);
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
name: variable.name,
|
||||||
|
kind: 'variable',
|
||||||
|
type: symbol, get definition() { return getDefinitionOf(info, variable); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a more specific type for the variable in `templateElement` by inspecting
|
||||||
|
* all variables that are in scope in the `mergedTable`. This function is a special
|
||||||
|
* case for `ngFor` and `ngIf`. If resolution fails, return the `any` type.
|
||||||
|
* @param mergedTable symbol table for all variables in scope
|
||||||
|
* @param query
|
||||||
|
* @param templateElement
|
||||||
|
*/
|
||||||
function refinedVariableType(
|
function refinedVariableType(
|
||||||
type: Symbol, info: DiagnosticTemplateInfo, templateElement: EmbeddedTemplateAst): Symbol {
|
mergedTable: SymbolTable, query: SymbolQuery, templateElement: EmbeddedTemplateAst): Symbol {
|
||||||
// Special case the ngFor directive
|
// Special case the 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);
|
||||||
@ -144,9 +169,9 @@ function refinedVariableType(
|
|||||||
if (ngForDirective) {
|
if (ngForDirective) {
|
||||||
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
|
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
|
||||||
if (ngForOfBinding) {
|
if (ngForOfBinding) {
|
||||||
const bindingType = new AstType(info.members, info.query, {}).getType(ngForOfBinding.value);
|
const bindingType = new AstType(mergedTable, query, {}).getType(ngForOfBinding.value);
|
||||||
if (bindingType) {
|
if (bindingType) {
|
||||||
const result = info.query.getElementType(bindingType);
|
const result = query.getElementType(bindingType);
|
||||||
if (result) {
|
if (result) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -160,7 +185,7 @@ function refinedVariableType(
|
|||||||
if (ngIfDirective) {
|
if (ngIfDirective) {
|
||||||
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
|
const ngIfBinding = ngIfDirective.inputs.find(i => i.directiveName === 'ngIf');
|
||||||
if (ngIfBinding) {
|
if (ngIfBinding) {
|
||||||
const bindingType = new AstType(info.members, info.query, {}).getType(ngIfBinding.value);
|
const bindingType = new AstType(mergedTable, query, {}).getType(ngIfBinding.value);
|
||||||
if (bindingType) {
|
if (bindingType) {
|
||||||
return bindingType;
|
return bindingType;
|
||||||
}
|
}
|
||||||
@ -168,7 +193,7 @@ function refinedVariableType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We can't do better, return any
|
// We can't do better, return any
|
||||||
return info.query.getBuiltinType(BuiltinType.Any);
|
return query.getBuiltinType(BuiltinType.Any);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEventDeclaration(info: DiagnosticTemplateInfo, includeEvent?: boolean) {
|
function getEventDeclaration(info: DiagnosticTemplateInfo, includeEvent?: boolean) {
|
||||||
|
@ -283,6 +283,20 @@ describe('completions', () => {
|
|||||||
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
|
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
|
||||||
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
|
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to resolve variable in nested loop', () => {
|
||||||
|
mockHost.override(TEST_TEMPLATE, `
|
||||||
|
<div *ngFor="let leagueMembers of league">
|
||||||
|
<div *ngFor="let member of leagueMembers">
|
||||||
|
{{member.~{position}}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'position');
|
||||||
|
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
|
||||||
|
// member variable of type Hero has properties 'id' and 'name'.
|
||||||
|
expectContain(completions, CompletionKind.PROPERTY, ['id', 'name']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('data binding', () => {
|
describe('data binding', () => {
|
||||||
|
@ -173,6 +173,23 @@ describe('diagnostics', () => {
|
|||||||
expect(diagnostics).toEqual([]);
|
expect(diagnostics).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report diagnostic for invalid property in nested ngFor', () => {
|
||||||
|
const content = mockHost.override(TEST_TEMPLATE, `
|
||||||
|
<div *ngFor="let leagueMembers of league">
|
||||||
|
<div *ngFor="let member of leagueMembers">
|
||||||
|
{{member.xyz}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
const diagnostics = ngLS.getDiagnostics(TEST_TEMPLATE);
|
||||||
|
expect(diagnostics.length).toBe(1);
|
||||||
|
const {messageText, start, length} = diagnostics[0];
|
||||||
|
expect(messageText)
|
||||||
|
.toBe(`Identifier 'xyz' is not defined. 'Hero' does not contain such a member`);
|
||||||
|
expect(start).toBe(content.indexOf('member.xyz'));
|
||||||
|
expect(length).toBe('member.xyz'.length);
|
||||||
|
});
|
||||||
|
|
||||||
describe('with $event', () => {
|
describe('with $event', () => {
|
||||||
it('should accept an event', () => {
|
it('should accept an event', () => {
|
||||||
const fileName = '/app/test.ng';
|
const fileName = '/app/test.ng';
|
||||||
|
@ -189,6 +189,7 @@ export class TemplateReference {
|
|||||||
title = 'Some title';
|
title = 'Some title';
|
||||||
hero: Hero = {id: 1, name: 'Windstorm'};
|
hero: Hero = {id: 1, name: 'Windstorm'};
|
||||||
heroes: Hero[] = [this.hero];
|
heroes: Hero[] = [this.hero];
|
||||||
|
league: Hero[][] = [this.heroes];
|
||||||
anyValue: any;
|
anyValue: any;
|
||||||
myClick(event: any) {}
|
myClick(event: any) {}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user