/** * @license * Copyright Google Inc. 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 {Diagnostics, LanguageService} from '../src/types'; import {TypeScriptServiceHost} from '../src/typescript_host'; import {toh} from './test_data'; import {MockTypescriptHost, diagnosticMessageContains, findDiagnostic, includeDiagnostic, noDiagnostics} from './test_utils'; describe('diagnostics', () => { let mockHost: MockTypescriptHost; let ngHost: TypeScriptServiceHost; let ngService: 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); ngHost.setSite(ngService); }); it('should be no diagnostics for test.ng', () => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); }); describe('for semantic errors', () => { const fileName = '/app/test.ng'; function diagnostics(template: string): Diagnostics { 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'); }); }); }); 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.span.end - diagnostic.span.start).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); 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.updateAnalyzedModules(); try { cb(fileName, newContent); } finally { mockHost.override(fileName, undefined !); } } function expectOnlyModuleDiagnostics(diagnostics: Diagnostics | undefined) { // Expect only the 'MyComponent' diagnostic if (!diagnostics) throw new Error('Expecting Diagnostics'); if (diagnostics.length > 1) { const unexpectedDiagnostics = diagnostics.filter(diag => !diagnosticMessageContains(diag.message, 'MyComponent')) .map(diag => `(${diag.span.start}:${diag.span.end}): ${diag.message}`); if (unexpectedDiagnostics.length) { fail(`Unexpected diagnostics:\n ${unexpectedDiagnostics.join('\n ')}`); return; } } expect(diagnostics.length).toBe(1); expect(diagnosticMessageContains(diagnostics[0].message, 'MyComponent')).toBeTruthy(); } }); });