/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {MockTypescriptHost} from './test_utils';
/**
* Note: If we want to test that a specific diagnostic message is emitted, then
* use the `mockHost.addCode()` helper method to add code to an existing file and check
* that the diagnostic messages contain the expected output.
*
* If the goal is to assert that there is no error in a specific file, then use
* `mockHost.override()` method to completely override an existing file, and
* make sure no diagnostics are produced. When doing so, be extra cautious
* about import statements and make sure to assert empty TS diagnostic messages
* as well.
*/
const TEST_TEMPLATE = '/app/test.ng';
const APP_COMPONENT = '/app/app.component.ts';
describe('diagnostics', () => {
const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
const tsLS = ts.createLanguageService(mockHost);
const ngHost = new TypeScriptServiceHost(mockHost, tsLS);
const ngLS = createLanguageService(ngHost);
beforeEach(() => {
mockHost.reset();
});
it('should produce no diagnostics for test.ng', () => {
// there should not be any errors on existing external template
expect(ngLS.getSemanticDiagnostics('/app/test.ng')).toEqual([]);
});
it('should not return TS and NG errors for existing files', () => {
const files = [
'/app/app.component.ts',
'/app/main.ts',
];
for (const file of files) {
const syntaxDiags = tsLS.getSyntacticDiagnostics(file);
expect(syntaxDiags).toEqual([]);
const semanticDiags = tsLS.getSemanticDiagnostics(file);
expect(semanticDiags).toEqual([]);
const ngDiags = ngLS.getSemanticDiagnostics(file);
expect(ngDiags).toEqual([]);
}
});
it('should report error for unexpected end of expression', () => {
const content = mockHost.override(TEST_TEMPLATE, `{{ 5 / }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length} = diags[0];
expect(messageText)
.toBe(
'Parser Error: Unexpected end of expression: {{ 5 / }} ' +
'at the end of the expression [{{ 5 / }}] in /app/test.ng@0:0');
expect(start).toBe(0);
expect(length).toBe(content.length);
});
// https://github.com/angular/vscode-ng-language-service/issues/242
it('should support $any() type cast function', () => {
mockHost.override(TEST_TEMPLATE, `
{{$any(title).xyz}}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags).toEqual([]);
});
it('should report error for $any() with incorrect number of arguments', () => {
const templates = [
'
{{$any().xyz}}
', // no argument
'
{{$any(title, title).xyz}}
', // two arguments
];
for (const template of templates) {
mockHost.override(TEST_TEMPLATE, template);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe('Unable to resolve signature for call of $any');
}
});
it('should not produce diagnostics for absolute template url', () => {
mockHost.override(APP_COMPONENT, `
import {Component} from '@angular/core';
@Component({
templateUrl: '${TEST_TEMPLATE}',
})
export class AppComponent {}
`);
const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT);
expect(diags).toEqual([]);
});
it('should not produce diagnostics for slice pipe with arguments', () => {
mockHost.override(TEST_TEMPLATE, `
{{h.name}}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags).toEqual([]);
});
it('should produce diagnostics for slice pipe with args when member is invalid', () => {
mockHost.override(TEST_TEMPLATE, `
{{h.age}}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Identifier 'age' is not defined. 'Hero' does not contain such a member`);
});
it('should not report error for variable initialized as class method', () => {
mockHost.override(TEST_TEMPLATE, `
`);
const diagnostics = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diagnostics).toEqual([]);
});
describe('diagnostics for expression comparisons', () => {
for (let [left, right, leftTy, rightTy] of [
['\'abc\'', 1, 'string', 'number'],
['hero', 2, 'object', 'number'],
['strOrNumber', 'hero', 'string|number', 'object'],
]) {
it(`it should report errors for mismtched types in a comparison: ${leftTy} and ${rightTy}`,
() => {
mockHost.override(TEST_TEMPLATE, `{{ ${left} != ${right} }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Expected operands to be of comparable types or any`);
});
}
for (let [left, right, leftTy, rightTy] of [
['\'abc\'', 'anyValue', 'string', 'any'],
['\'abc\'', null, 'string', 'null'],
['\'abc\'', undefined, 'string', 'undefined'],
[null, null, 'null', 'null'],
['{a: 1}', '{b: 2}', 'object', 'object'],
['strOrNumber', '1', 'string|number', 'number'],
]) {
it(`it should not report errors for compatible types in a comparison: ${leftTy} and ${
rightTy}`,
() => {
mockHost.override(TEST_TEMPLATE, `{{ ${left} != ${right} }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(0);
});
}
});
describe('diagnostics for ngFor exported values', () => {
it('should report errors for mismatched exported types', () => {
mockHost.override(TEST_TEMPLATE, `
'i' is a number; 'isFirst' is a boolean
{{ i === isFirst }}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Expected operands to be of comparable types or any`);
});
it('should not report errors for matching exported type', () => {
mockHost.override(TEST_TEMPLATE, `
'i' is a number
{{ i < 2 }}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(0);
});
});
describe('diagnostics for ngIf exported values', () => {
it('should infer the type of an implicit value in an NgIf context', () => {
mockHost.override(TEST_TEMPLATE, `
'titleProxy' is a string
{{titleProxy.~{start-err}notAProperty~{end-err}}}
`);
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, `
'titleProxy' is a string
{{titleProxy.~{start-err}notAProperty~{end-err}}}
`);
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', () => {
it('should work with numeric index signatures (arrays)', () => {
mockHost.override(TEST_TEMPLATE, `
{{heroes[0].badProperty}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
});
describe('with string index signatures', () => {
it('should work with index notation', () => {
mockHost.override(TEST_TEMPLATE, `
{{heroesByName['Jacky'].badProperty}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
});
it('should work with dot notation', () => {
mockHost.override(TEST_TEMPLATE, `
{{heroesByName.jacky.badProperty}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
});
it('should not produce errors with dot notation if stringIndexType is a primitive type',
() => {
mockHost.override(TEST_TEMPLATE, `{{primitiveIndexType.test}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(0);
});
});
});
it('should produce diagnostics for invalid tuple type property access', () => {
mockHost.override(TEST_TEMPLATE, `
{{tupleArray[1].badProperty}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
});
it('should not produce errors if tuple array index out of bound', () => {
mockHost.override(TEST_TEMPLATE, `
{{tupleArray[2].badProperty}}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags).toEqual([]);
});
it('should not produce errors on function.bind()', () => {
mockHost.override(TEST_TEMPLATE, `
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags).toEqual([]);
});
it('should report access to an unknown field', () => {
mockHost.override(TEST_TEMPLATE, `{{ foo }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain(
`Identifier 'foo' is not defined. ` +
`The component declaration, template variable declarations, ` +
`and element references do not contain such a member`);
});
it('should report access to an unknown sub-field', () => {
mockHost.override(TEST_TEMPLATE, `{{ hero.nam }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain(
`Identifier 'nam' is not defined. 'Hero' does not contain such a member`);
});
it('should report access to a private member', () => {
mockHost.override(TEST_TEMPLATE, `{{ myField }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain(`Identifier 'myField' refers to a private member of the component`);
});
it('should report numeric operator errors', () => {
mockHost.override(TEST_TEMPLATE, `{{ 'a' % 2 }}`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain('Expected a number type');
});
it('should report an unknown field', () => {
mockHost.override(TEST_TEMPLATE, ``);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain(
`Identifier 'people' is not defined. ` +
`The component declaration, template variable declarations, ` +
`and element references do not contain such a member`);
});
it('should report an unknown value in a key expression', () => {
mockHost.override(TEST_TEMPLATE, ``);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE).map(d => d.messageText);
expect(diags).toContain(
`Identifier 'trackByFn' is not defined. ` +
`The component declaration, template variable declarations, ` +
`and element references do not contain such a member`);
});
describe('embedded templates', () => {
it('should suggest refining a template context missing a property', () => {
mockHost.override(
TEST_TEMPLATE,
``);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length, category} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Suggestion);
expect(messageText)
.toBe(
`The template context of 'CounterDirective' does not define an implicit value.\n` +
`If the context type is a base type or 'any', consider refining it to a more specific type.`,
);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
it('should report an unknown context reference', () => {
mockHost.override(
TEST_TEMPLATE,
``);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length, category} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Suggestion);
expect(messageText)
.toBe(
`The template context of 'NgForOf' does not define a member called 'even_1'.\n` +
`If the context type is a base type or 'any', consider refining it to a more specific type.`);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
it('report an unknown field in $implicit context', () => {
mockHost.override(TEST_TEMPLATE, `
{{ myVar.~{start-emb}missingField~{end-emb} }}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length, category} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(messageText)
.toBe(
`Identifier 'missingField' is not defined. '{ implicitPerson: Hero; }' does not contain such a member`,
);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
it('report an unknown field in non implicit context', () => {
mockHost.override(TEST_TEMPLATE, `
{{ myVar.~{start-emb}missingField~{end-emb} }}
`);
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
expect(diags.length).toBe(1);
const {messageText, start, length, category} = diags[0];
expect(category).toBe(ts.DiagnosticCategory.Error);
expect(messageText)
.toBe(
`Identifier 'missingField' is not defined. 'Hero' does not contain such a member`,
);
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
expect(start).toBe(span.start);
expect(length).toBe(span.length);
});
});
// #17611
it('should not report diagnostic on iteration of any', () => {
const fileName = '/app/test.ng';
mockHost.override(fileName, '