diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts
index 823bcad12b..82ea25bb30 100644
--- a/packages/language-service/test/diagnostics_spec.ts
+++ b/packages/language-service/test/diagnostics_spec.ts
@@ -7,393 +7,480 @@
*/
import * as ts from 'typescript';
-
import {createLanguageService} from '../src/language_service';
-import {Diagnostics, LanguageService} from '../src/types';
+import * as ng from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
-
import {toh} from './test_data';
-import {MockTypescriptHost, diagnosticMessageContains, findDiagnostic, includeDiagnostic, noDiagnostics} from './test_utils';
+import {MockTypescriptHost} from './test_utils';
+
+/**
+ * Note: If we want to test that a specific diagnostic message is emitted, then
+ * use the `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.
+ */
describe('diagnostics', () => {
let mockHost: MockTypescriptHost;
let ngHost: TypeScriptServiceHost;
- let ngService: LanguageService;
+ let tsLS: ts.LanguageService;
+ let ngLS: ng.LanguageService;
beforeEach(() => {
mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
- const documentRegistry = ts.createDocumentRegistry();
- const service = ts.createLanguageService(mockHost, documentRegistry);
- ngHost = new TypeScriptServiceHost(mockHost, service);
- ngService = createLanguageService(ngHost);
+ tsLS = ts.createLanguageService(mockHost);
+ ngHost = new TypeScriptServiceHost(mockHost, tsLS);
+ ngLS = createLanguageService(ngHost);
});
- it('should be no diagnostics for test.ng',
- () => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); });
+ it('should produce no diagnostics for test.ng', () => {
+ // there should not be any errors on existing external template
+ expect(ngLS.getDiagnostics('/app/test.ng')).toEqual([]);
+ });
- describe('for semantic errors', () => {
+ 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.getDiagnostics(file);
+ expect(ngDiags).toEqual([]);
+ }
+ });
+
+ // #17611
+ it('should not report diagnostic on iteration of any', () => {
const fileName = '/app/test.ng';
-
- function diagnostics(template: string): ts.Diagnostic[] {
- try {
- mockHost.override(fileName, template);
- return ngService.getDiagnostics(fileName) !;
- } finally {
- mockHost.override(fileName, undefined !);
- }
- }
-
- function accept(template: string) { noDiagnostics(diagnostics(template)); }
-
- function reject(template: string, message: string): void;
- function reject(template: string, message: string, at: string): void;
- function reject(template: string, message: string, location: string): void;
- function reject(template: string, message: string, location: string, len: number): void;
- function reject(template: string, message: string, at?: number | string, len?: number): void {
- if (typeof at == 'string') {
- len = at.length;
- at = template.indexOf(at);
- }
- includeDiagnostic(diagnostics(template), message, at, len);
- }
-
- describe('regression', () => {
- it('should be able to return diagnostics if reflector gets invalidated', () => {
- const fileName = '/app/main.ts';
- ngService.getDiagnostics(fileName);
- (ngHost as any)._reflector = null;
- ngService.getDiagnostics(fileName);
- });
-
- // #17611
- it('should not report diagnostic on iteration of any',
- () => { accept('
{{value.someField}}
'); });
- });
-
- describe('with $event', () => {
- it('should accept an event',
- () => { accept('Click me!
'); });
- it('should reject it when not in an event binding', () => {
- reject('', '\'$event\' is not defined', '$event');
- });
- });
+ mockHost.override(fileName, '{{value.someField}}
');
+ const diagnostics = ngLS.getDiagnostics(fileName);
+ expect(diagnostics).toEqual([]);
});
- describe('with regression tests', () => {
-
- it('should not crash with a incomplete *ngFor', () => {
- expect(() => {
- const code =
- '\n@Component({template: \' ~{after-div}\'}) export class MyComponent {}';
- addCode(code, fileName => { ngService.getDiagnostics(fileName); });
- }).not.toThrow();
- });
-
- it('should report a component not in a module', () => {
- const code = '\n@Component({template: \'\'}) export class MyComponent {}';
- addCode(code, (fileName, content) => {
- const diagnostics = ngService.getDiagnostics(fileName);
- const offset = content !.lastIndexOf('@Component') + 1;
- const len = 'Component'.length;
- includeDiagnostic(
- diagnostics !, 'Component \'MyComponent\' is not included in a module', offset, len);
- });
- });
-
- it('should not report an error for a form\'s host directives', () => {
- const code = '\n@Component({template: \'\'}) export class MyComponent {}';
- addCode(code, fileName => {
- const diagnostics = ngService.getDiagnostics(fileName);
- expectOnlyModuleDiagnostics(diagnostics);
- });
- });
-
- it('should not throw getting diagnostics for an index expression', () => {
- const code =
- ` @Component({template: ''}) export class MyComponent {}`;
- addCode(
- code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
- });
-
- it('should not throw using a directive with no value', () => {
- const code =
- ` @Component({template: ''}) export class MyComponent { name = 'some name'; }`;
- addCode(
- code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
- });
-
- it('should report an error for invalid metadata', () => {
- const code =
- ` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`;
- addCode(code, (fileName, content) => {
- const diagnostics = ngService.getDiagnostics(fileName);
- includeDiagnostic(
- diagnostics !, 'Function expressions are not supported in decorators', '() => \'foo\'',
- content);
- });
- });
-
- it('should not throw for an invalid class', () => {
- const code = ` @Component({template: ''}) class`;
- addCode(
- code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
- });
-
- it('should not report an error for sub-types of string', () => {
- const code =
- ` @Component({template: \`\`}) export class MyComponent { something: 'foo' | 'bar'; }`;
- addCode(code, fileName => {
- const diagnostics = ngService.getDiagnostics(fileName);
- expectOnlyModuleDiagnostics(diagnostics);
- });
- });
-
- it('should not report an error for sub-types of number', () => {
- const code =
- ` @Component({template: \`\`}) export class MyComponent { something: 123 | 456; }`;
- addCode(code, fileName => {
- const diagnostics = ngService.getDiagnostics(fileName);
- expectOnlyModuleDiagnostics(diagnostics);
- });
- });
-
- it('should report a warning if an event results in a callable expression', () => {
- const code =
- ` @Component({template: \`\`}) export class MyComponent { onClick() { } }`;
- addCode(code, (fileName, content) => {
- const diagnostics = ngService.getDiagnostics(fileName);
- includeDiagnostic(
- diagnostics !, 'Unexpected callable expression. Expected a method call', 'onClick',
- content);
- });
- });
-
- // #13412
- it('should not report an error for using undefined', () => {
- const code =
- ` @Component({template: \`\`}) export class MyComponent { something = 'foo'; }})`;
- addCode(code, fileName => {
- const diagnostics = ngService.getDiagnostics(fileName);
- expectOnlyModuleDiagnostics(diagnostics);
- });
- });
-
- // Issue #13326
- it('should report a narrow span for invalid pipes', () => {
- const code =
- ` @Component({template: ' Using an invalid pipe {{data | dat}}
'}) export class MyComponent { data = 'some data'; }`;
- addCode(code, fileName => {
- const diagnostic = findDiagnostic(ngService.getDiagnostics(fileName) !, 'pipe') !;
- expect(diagnostic).not.toBeUndefined();
- expect(diagnostic.length).toBeLessThan(11);
- });
- });
-
- // Issue #19406
- it('should allow empty template', () => {
- const appComponent = `
- import { Component } from '@angular/core';
-
- @Component({
- template : '',
- })
- export class AppComponent {}
- `;
- const fileName = '/app/app.component.ts';
- mockHost.override(fileName, appComponent);
- const diagnostics = ngService.getDiagnostics(fileName);
+ describe('with $event', () => {
+ it('should accept an event', () => {
+ const fileName = '/app/test.ng';
+ mockHost.override(fileName, 'Click me!
');
+ const diagnostics = ngLS.getDiagnostics(fileName);
expect(diagnostics).toEqual([]);
});
- // Issue #15460
- it('should be able to find members defined on an ancestor type', () => {
- const app_component = `
- import { Component } from '@angular/core';
- import { NgForm } from '@angular/common';
-
- @Component({
- selector: 'example-app',
- template: \`
-
- First name value: {{ first.value }}
- First name valid: {{ first.valid }}
- Form value: {{ f.value | json }}
- Form valid: {{ f.valid }}
- \`,
- })
- export class AppComponent {
- onSubmit(form: NgForm) {}
- }
- `;
- const fileName = '/app/app.component.ts';
- mockHost.override(fileName, app_component);
- const diagnostic = ngService.getDiagnostics(fileName);
- expect(diagnostic).toEqual([]);
- });
-
- it('should report an error for invalid providers', () => {
- addCode(
- `
- @Component({
- template: '',
- providers: [null]
- })
- export class MyComponent {}
- `,
- fileName => {
- const diagnostics = ngService.getDiagnostics(fileName) !;
- const expected = findDiagnostic(diagnostics, 'Invalid providers for');
- const notExpected = findDiagnostic(diagnostics, 'Cannot read property');
- expect(expected).toBeDefined();
- expect(notExpected).toBeUndefined();
- });
- });
-
- // Issue #15768
- it('should be able to parse a template reference', () => {
- addCode(
- `
- @Component({
- selector: 'my-component',
- template: \`
-
-
- Loading comps...
- \`
- })
- export class MyComponent {}
- `,
- fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
- });
-
- // Issue #15625
- it('should not report errors for localization syntax', () => {
- addCode(
- `
- @Component({
- selector: 'my-component',
- template: \`
-
- {fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
-
- \`
- })
- export class MyComponent {
- fieldCount: number;
- }
- `,
- fileName => {
- const diagnostics = ngService.getDiagnostics(fileName);
- expectOnlyModuleDiagnostics(diagnostics);
- });
- });
-
- // Issue #15885
- it('should be able to remove null and undefined from a type', () => {
- mockHost.overrideOptions(options => {
- options.strictNullChecks = true;
- return options;
- });
- addCode(
- `
- @Component({
- selector: 'my-component',
- template: \` {{test?.a}}
- \`
- })
- export class MyComponent {
- test: {a: number, b: number} | null = {
- a: 1,
- b: 2
- };
- }
- `,
- fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName)));
- });
-
- it('should be able to resolve modules using baseUrl', () => {
- const app_component = `
- import { Component } from '@angular/core';
- import { NgForm } from '@angular/common';
- import { Server } from 'app/server';
-
- @Component({
- selector: 'example-app',
- template: '...',
- providers: [Server]
- })
- export class AppComponent {
- onSubmit(form: NgForm) {}
- }
- `;
- const app_server = `
- export class Server {}
- `;
- const fileName = '/app/app.component.ts';
- mockHost.override(fileName, app_component);
- mockHost.addScript('/other/files/app/server.ts', app_server);
- mockHost.overrideOptions(options => {
- options.baseUrl = '/other/files';
- return options;
- });
- const diagnostic = ngService.getDiagnostics(fileName);
- expect(diagnostic).toEqual([]);
- });
-
- it('should not report errors for using the now removed OpaqueToken (support for v4)', () => {
- const app_component = `
- import { Component, Inject, OpaqueToken } from '@angular/core';
- import { NgForm } from '@angular/common';
-
- export const token = new OpaqueToken();
-
- @Component({
- selector: 'example-app',
- template: '...'
- })
- export class AppComponent {
- constructor (@Inject(token) value: string) {}
- onSubmit(form: NgForm) {}
- }
- `;
- const fileName = '/app/app.component.ts';
- mockHost.override(fileName, app_component);
- const diagnostics = ngService.getDiagnostics(fileName);
- expect(diagnostics).toEqual([]);
- });
-
- function addCode(code: string, cb: (fileName: string, content?: string) => void) {
- const fileName = '/app/app.component.ts';
- const originalContent = mockHost.getFileContent(fileName);
- const newContent = originalContent + code;
- mockHost.override(fileName, originalContent + code);
- ngHost.getAnalyzedModules();
- try {
- cb(fileName, newContent);
- } finally {
- mockHost.override(fileName, undefined !);
- }
- }
-
- function expectOnlyModuleDiagnostics(diagnostics: ts.Diagnostic[] | undefined) {
- // Expect only the 'MyComponent' diagnostic
- if (!diagnostics) throw new Error('Expecting Diagnostics');
- if (diagnostics.length > 1) {
- const unexpectedDiagnostics =
- diagnostics.filter(diag => !diagnosticMessageContains(diag.messageText, 'MyComponent'))
- .map(diag => `(${diag.start}:${diag.start! + diag.length!}): ${diag.messageText}`);
-
- if (unexpectedDiagnostics.length) {
- fail(`Unexpected diagnostics:\n ${unexpectedDiagnostics.join('\n ')}`);
- return;
- }
- }
+ it('should reject it when not in an event binding', () => {
+ const fileName = '/app/test.ng';
+ const content = mockHost.override(fileName, '');
+ const diagnostics = ngLS.getDiagnostics(fileName) !;
expect(diagnostics.length).toBe(1);
- expect(diagnosticMessageContains(diagnostics[0].messageText, 'MyComponent')).toBeTruthy();
- }
+ const {messageText, start, length} = diagnostics[0];
+ expect(messageText)
+ .toBe(
+ 'Identifier \'$event\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
+ const keyword = '$event';
+ expect(start).toBe(content.lastIndexOf(keyword));
+ expect(length).toBe(keyword.length);
+ });
});
+
+ it('should not crash with a incomplete *ngFor', () => {
+ const fileName = addCode(`
+ @Component({
+ template: ' ~{after-div}'
+ })
+ export class MyComponent {}`);
+ expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
+ });
+
+ it('should report a component not in a module', () => {
+ const fileName = addCode(`
+ @Component({
+ template: ''
+ })
+ export class MyComponent {}`);
+ const diagnostics = ngLS.getDiagnostics(fileName) !;
+ expect(diagnostics.length).toBe(1);
+ const {messageText, start, length} = diagnostics[0];
+ expect(messageText)
+ .toBe(
+ 'Component \'MyComponent\' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.');
+ const content = mockHost.getFileContent(fileName) !;
+ const keyword = '@Component';
+ expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
+ expect(length).toBe(keyword.length - 1); // exclude leading '@'
+ });
+
+
+ it(`should not report an error for a form's host directives`, () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: ''})
+ export class AppComponent {}`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ it('should not throw getting diagnostics for an index expression', () => {
+ const fileName = addCode(`
+ @Component({
+ template: ''
+ })
+ export class MyComponent {}`);
+ expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
+ });
+
+ it('should not throw using a directive with no value', () => {
+ const fileName = addCode(`
+ @Component({
+ template: ''
+ })
+ export class MyComponent {
+ name = 'some name';
+ }`);
+ expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
+ });
+
+ it('should report an error for invalid metadata', () => {
+ const fileName = '/app/app.component.ts';
+ const content = mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: '',
+ providers: [
+ {provide: 'foo', useFactory: () => 'foo' }
+ ]
+ })
+ export class AppComponent {
+ name = 'some name';
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName) !;
+ expect(ngDiags.length).toBe(1);
+ const {messageText, start, length} = ngDiags[0];
+ const keyword = `() => 'foo'`;
+ expect(start).toBe(content.lastIndexOf(keyword));
+ expect(length).toBe(keyword.length);
+ // messageText is a three-part chain
+ const firstPart = messageText as ts.DiagnosticMessageChain;
+ expect(firstPart.messageText).toBe('Error during template compile of \'AppComponent\'');
+ const secondPart = firstPart.next !;
+ expect(secondPart.messageText).toBe('Function expressions are not supported in decorators');
+ const thirdPart = secondPart.next !;
+ expect(thirdPart.messageText)
+ .toBe('Consider changing the function expression into an exported function');
+ expect(thirdPart.next).toBeFalsy();
+ });
+
+ it('should not throw for an invalid class', () => {
+ const fileName = addCode(`
+ @Component({
+ template: ''
+ }) class`);
+ expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
+ });
+
+ it('should not report an error for sub-types of string', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: \`\`
+ })
+ export class AppComponent {
+ something: 'foo' | 'bar';
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ it('should not report an error for sub-types of number', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: ''
+ })
+ export class AppComponent {
+ something: 123 | 456;
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ it('should report a warning if an event results in a callable expression', () => {
+ const fileName = '/app/app.component.ts';
+ const content = mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: ''
+ })
+ export class MyComponent {
+ onClick() { }
+ }`);
+ const diagnostics = ngLS.getDiagnostics(fileName) !;
+ const {messageText, start, length} = diagnostics[0];
+ expect(messageText).toBe('Unexpected callable expression. Expected a method call');
+ const keyword = `"onClick"`;
+ expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading quote
+ expect(length).toBe(keyword.length - 2); // exclude leading and trailing quotes
+ });
+
+ // #13412
+ it('should not report an error for using undefined', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: ''
+ })
+ export class AppComponent {
+ something = 'foo';
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ // Issue #13326
+ it('should report a narrow span for invalid pipes', () => {
+ const fileName = '/app/app.component.ts';
+ const content = mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: ' Using an invalid pipe {{data | dat}}
'
+ })
+ export class AppComponent {
+ data = 'some data';
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags.length).toBe(1);
+ const {messageText, start, length} = ngDiags[0];
+ expect(messageText).toBe(`The pipe 'dat' could not be found`);
+ const keyword = 'data | dat';
+ expect(start).toBe(content.lastIndexOf(keyword));
+ expect(length).toBe(keyword.length);
+ });
+
+ // Issue #19406
+ it('should allow empty template', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template : '',
+ })
+ export class AppComponent {}`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ // Issue #15460
+ it('should be able to find members defined on an ancestor type', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+ import { NgForm } from '@angular/forms';
+
+ @Component({
+ selector: 'example-app',
+ template: \`
+
+ First name value: {{ first.value }}
+ First name valid: {{ first.valid }}
+ Form value: {{ f.value | json }}
+ Form valid: {{ f.valid }}
+ \`,
+ })
+ export class AppComponent {
+ onSubmit(form: NgForm) {}
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ it('should report an error for invalid providers', () => {
+ const fileName = '/app/app.component.ts';
+ const content = mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ template: '',
+ providers: [null]
+ })
+ export class AppComponent {}`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags.length).toBe(1);
+ const {messageText, start, length} = ngDiags[0];
+ expect(messageText)
+ .toBe(
+ 'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]');
+ // TODO: Looks like this is the wrong span. Should point to 'null' instead.
+ const keyword = '@Component';
+ expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
+ expect(length).toBe(keyword.length - 1); // exclude leading '@
+ });
+
+ // Issue #15768
+ it('should be able to parse a template reference', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ selector: 'my-component',
+ template: \`
+
+
+ Loading comps...
+ \`
+ })
+ export class AppComponent {}`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ // Issue #15625
+ it('should not report errors for localization syntax', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ selector: 'my-component',
+ template: \`
+
+ {fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
+
+ \`
+ })
+ export class AppComponent {
+ fieldCount: number;
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ // Issue #15885
+ it('should be able to remove null and undefined from a type', () => {
+ mockHost.overrideOptions(options => {
+ options.strictNullChecks = true;
+ return options;
+ });
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+
+ @Component({
+ selector: 'my-component',
+ template: '{{test?.a}}',
+ })
+ export class AppComponent {
+ test: {a: number, b: number} | null = {
+ a: 1,
+ b: 2,
+ };
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const ngDiags = ngLS.getDiagnostics(fileName);
+ expect(ngDiags).toEqual([]);
+ });
+
+ it('should be able to resolve modules using baseUrl', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component } from '@angular/core';
+ import { NgForm } from '@angular/forms';
+ import { Server } from 'app/server';
+
+ @Component({
+ selector: 'example-app',
+ template: '...',
+ providers: [Server]
+ })
+ export class AppComponent {
+ onSubmit(form: NgForm) {}
+ }`);
+ mockHost.addScript('/other/files/app/server.ts', 'export class Server {}');
+ mockHost.overrideOptions(options => {
+ options.baseUrl = '/other/files';
+ return options;
+ });
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags).toEqual([]);
+ const diagnostic = ngLS.getDiagnostics(fileName);
+ expect(diagnostic).toEqual([]);
+ });
+
+ it('should report errors for using the now removed OpaqueToken (deprecated)', () => {
+ const fileName = '/app/app.component.ts';
+ mockHost.override(fileName, `
+ import { Component, Inject, OpaqueToken } from '@angular/core';
+ import { NgForm } from '@angular/forms';
+
+ export const token = new OpaqueToken('some token');
+
+ @Component({
+ selector: 'example-app',
+ template: '...'
+ })
+ export class AppComponent {
+ constructor (@Inject(token) value: string) {}
+ onSubmit(form: NgForm) {}
+ }`);
+ const tsDiags = tsLS.getSemanticDiagnostics(fileName);
+ expect(tsDiags.length).toBe(1);
+ expect(tsDiags[0].messageText)
+ .toBe(
+ `Module '"../node_modules/@angular/core/core"' has no exported member 'OpaqueToken'.`);
+ });
+
+ function addCode(code: string) {
+ const fileName = '/app/app.component.ts';
+ const originalContent = mockHost.getFileContent(fileName);
+ const newContent = originalContent + code;
+ mockHost.override(fileName, newContent);
+ return fileName;
+ }
+
});
diff --git a/packages/language-service/test/test_utils.ts b/packages/language-service/test/test_utils.ts
index 80dcddadb9..405ebbdcf5 100644
--- a/packages/language-service/test/test_utils.ts
+++ b/packages/language-service/test/test_utils.ts
@@ -11,7 +11,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
-import {Diagnostic, DiagnosticMessageChain, Diagnostics, Span} from '../src/types';
+import {Span} from '../src/types';
export type MockData = string | MockDirectory;
@@ -104,6 +104,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
} else {
this.overrides.delete(fileName);
}
+ return content;
}
addScript(fileName: string, content: string) {
@@ -330,53 +331,3 @@ function getReferenceMarkers(value: string): ReferenceResult {
function removeReferenceMarkers(value: string): string {
return value.replace(referenceMarker, (match, text) => text.replace(/ᐱ/g, ''));
}
-
-export function noDiagnostics(diagnostics: ts.Diagnostic[]) {
- if (diagnostics && diagnostics.length) {
- throw new Error(
- `Unexpected diagnostics: \n ${diagnostics.map(d => d.messageText).join('\n ')}`);
- }
-}
-
-export function diagnosticMessageContains(
- message: string | ts.DiagnosticMessageChain, messageFragment: string): boolean {
- if (typeof message == 'string') {
- return message.indexOf(messageFragment) >= 0;
- }
- if (message.messageText.indexOf(messageFragment) >= 0) {
- return true;
- }
- if (message.next) {
- return diagnosticMessageContains(message.next, messageFragment);
- }
- return false;
-}
-
-export function findDiagnostic(
- diagnostics: ts.Diagnostic[], messageFragment: string): ts.Diagnostic|undefined {
- return diagnostics.find(d => diagnosticMessageContains(d.messageText, messageFragment));
-}
-
-export function includeDiagnostic(
- diagnostics: ts.Diagnostic[], message: string, text?: string, len?: string): void;
-export function includeDiagnostic(
- diagnostics: ts.Diagnostic[], message: string, at?: number, len?: number): void;
-export function includeDiagnostic(
- diagnostics: ts.Diagnostic[], message: string, p1?: any, p2?: any) {
- expect(diagnostics).toBeDefined();
- if (diagnostics) {
- const diagnostic = findDiagnostic(diagnostics, message);
- expect(diagnostic).toBeDefined(`no diagnostic contains '${message}`);
- if (diagnostic && p1 != null) {
- const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
- const len = typeof p2 === 'number' ? p2 : p1.length;
- expect(diagnostic.start)
- .toEqual(
- at,
- `expected message '${message}' was reported at ${diagnostic.start} but should be ${at}`);
- if (len != null) {
- expect(diagnostic.length).toEqual(len, `expected '${message}'s span length to be ${len}`);
- }
- }
- }
-}