diff --git a/packages/language-service/src/expression_diagnostics.ts b/packages/language-service/src/expression_diagnostics.ts index 8acc2db926..159a014120 100644 --- a/packages/language-service/src/expression_diagnostics.ts +++ b/packages/language-service/src/expression_diagnostics.ts @@ -115,7 +115,7 @@ function getVarDeclarations( // that have been declared so far are also in scope. info.query.createSymbolTable(results), ]); - symbol = refinedVariableType(symbolsInScope, info.query, current); + symbol = refinedVariableType(variable.value, symbolsInScope, info.query, current); } results.push({ name: variable.name, @@ -127,16 +127,37 @@ function getVarDeclarations( return results; } +/** + * Gets the type of an ngFor exported value, as enumerated in + * https://angular.io/api/common/NgForOfContext + * @param value exported value name + * @param query type symbol query + */ +function getNgForExportedValueType(value: string, query: SymbolQuery): Symbol|undefined { + switch (value) { + case 'index': + case 'count': + return query.getBuiltinType(BuiltinType.Number); + case 'first': + case 'last': + case 'even': + case 'odd': + return query.getBuiltinType(BuiltinType.Boolean); + } +} + /** * 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 value variable value name * @param mergedTable symbol table for all variables in scope * @param query * @param templateElement */ function refinedVariableType( - mergedTable: SymbolTable, query: SymbolQuery, templateElement: EmbeddedTemplateAst): Symbol { + value: string, mergedTable: SymbolTable, query: SymbolQuery, + templateElement: EmbeddedTemplateAst): Symbol { // Special case the ngFor directive const ngForDirective = templateElement.directives.find(d => { const name = identifierName(d.directive.type); @@ -145,12 +166,17 @@ function refinedVariableType( if (ngForDirective) { const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf'); if (ngForOfBinding) { + // Check if the variable value is a type exported by the ngFor statement. + let result = getNgForExportedValueType(value, query); + + // Otherwise, check if there is a known type for the ngFor binding. const bindingType = new AstType(mergedTable, query, {}).getType(ngForOfBinding.value); - if (bindingType) { - const result = query.getElementType(bindingType); - if (result) { - return result; - } + if (!result && bindingType) { + result = query.getElementType(bindingType); + } + + if (result) { + return result; } } } diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index 5a21eb43a2..2317a85382 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -119,6 +119,31 @@ describe('diagnostics', () => { expect(diagnostics).toEqual([]); }); + describe('diagnostics for ngFor exported values', () => { + it('should report errors for mistmatched exported types', () => { + mockHost.override(TEST_TEMPLATE, ` +
+ 'i' is a number; 'isFirst' is a boolean + {{ i === isFirst }} +
+ `); + const diags = ngLS.getDiagnostics(TEST_TEMPLATE); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toBe(`Expected the operants to be of similar type or any`); + }); + + it('should not report errors for matching exported type', () => { + mockHost.override(TEST_TEMPLATE, ` +
+ 'i' is a number + {{ i < 2 }} +
+ `); + const diags = ngLS.getDiagnostics(TEST_TEMPLATE); + expect(diags.length).toBe(0); + }); + }); + describe('diagnostics for invalid indexed type property access', () => { it('should work with numeric index signatures (arrays)', () => { mockHost.override(TEST_TEMPLATE, `