2016-11-22 12:10:23 -05:00
/ * *
* @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' ;
2019-08-15 18:17:00 -04:00
import * as ng from '../src/types' ;
2016-11-22 12:10:23 -05:00
import { TypeScriptServiceHost } from '../src/typescript_host' ;
import { toh } from './test_data' ;
2019-08-15 18:17:00 -04:00
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 .
* /
2016-11-22 12:10:23 -05:00
describe ( 'diagnostics' , ( ) = > {
2018-07-06 02:13:25 -04:00
let mockHost : MockTypescriptHost ;
let ngHost : TypeScriptServiceHost ;
2019-08-15 18:17:00 -04:00
let tsLS : ts.LanguageService ;
let ngLS : ng.LanguageService ;
2018-07-06 02:13:25 -04:00
beforeEach ( ( ) = > {
mockHost = new MockTypescriptHost ( [ '/app/main.ts' , '/app/parsing-cases.ts' ] , toh ) ;
2019-08-15 18:17:00 -04:00
tsLS = ts . createLanguageService ( mockHost ) ;
ngHost = new TypeScriptServiceHost ( mockHost , tsLS ) ;
ngLS = createLanguageService ( ngHost ) ;
2018-07-06 02:13:25 -04:00
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
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 ( [ ] ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
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 ( [ ] ) ;
2016-11-22 12:10:23 -05:00
}
2019-08-15 18:17:00 -04:00
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
// #17611
it ( 'should not report diagnostic on iteration of any' , ( ) = > {
const fileName = '/app/test.ng' ;
mockHost . override ( fileName , '<div *ngFor="let value of anyValue">{{value.someField}}</div>' ) ;
const diagnostics = ngLS . getDiagnostics ( fileName ) ;
expect ( diagnostics ) . toEqual ( [ ] ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
describe ( 'with $event' , ( ) = > {
it ( 'should accept an event' , ( ) = > {
const fileName = '/app/test.ng' ;
mockHost . override ( fileName , '<div (click)="myClick($event)">Click me!</div>' ) ;
const diagnostics = ngLS . getDiagnostics ( fileName ) ;
expect ( diagnostics ) . toEqual ( [ ] ) ;
2017-03-28 12:37:24 -04:00
} ) ;
2019-08-15 18:17:00 -04:00
it ( 'should reject it when not in an event binding' , ( ) = > {
const fileName = '/app/test.ng' ;
const content = mockHost . override ( fileName , '<div [tabIndex]="$event"></div>' ) ;
const diagnostics = ngLS . getDiagnostics ( fileName ) ! ;
expect ( diagnostics . length ) . toBe ( 1 ) ;
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 ) ;
2016-11-22 12:10:23 -05:00
} ) ;
} ) ;
2019-08-15 18:17:00 -04:00
it ( 'should not crash with a incomplete *ngFor' , ( ) = > {
const fileName = addCode ( `
@Component ( {
template : '<div *ngFor></div> ~{after-div}'
} )
export class MyComponent { } ` );
expect ( ( ) = > ngLS . getDiagnostics ( fileName ) ) . not . toThrow ( ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
it ( 'should report a component not in a module' , ( ) = > {
const fileName = addCode ( `
@Component ( {
template : '<div></div>'
} )
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 '@'
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
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' ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
@Component ( {
template : '<form></form>' } )
export class AppComponent { } ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
it ( 'should not throw getting diagnostics for an index expression' , ( ) = > {
const fileName = addCode ( `
@Component ( {
template : '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'
} )
export class MyComponent { } ` );
expect ( ( ) = > ngLS . getDiagnostics ( fileName ) ) . not . toThrow ( ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
it ( 'should not throw using a directive with no value' , ( ) = > {
const fileName = addCode ( `
@Component ( {
template : '<form><input [(ngModel)]="name" required /></form>'
} )
export class MyComponent {
name = 'some name' ;
} ` );
expect ( ( ) = > ngLS . getDiagnostics ( fileName ) ) . not . toThrow ( ) ;
} ) ;
2016-11-22 12:10:23 -05:00
2019-08-15 18:17:00 -04:00
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 : '<div></div>' ,
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 ( ) ;
} ) ;
2016-12-06 12:56:30 -05:00
2019-08-15 18:17:00 -04:00
it ( 'should not throw for an invalid class' , ( ) = > {
const fileName = addCode ( `
@Component ( {
template : ''
} ) class ` );
expect ( ( ) = > ngLS . getDiagnostics ( fileName ) ) . not . toThrow ( ) ;
} ) ;
2018-12-14 16:40:01 -05:00
2019-08-15 18:17:00 -04:00
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 : \ ` <div *ngIf="something === 'foo'"></div> \`
} )
export class AppComponent {
something : 'foo' | 'bar' ;
} ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2016-12-13 14:20:45 -05:00
2019-08-15 18:17:00 -04:00
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 : '<div *ngIf="something === 123"></div>'
} )
export class AppComponent {
something : 123 | 456 ;
} ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2016-12-12 21:11:56 -05:00
2019-08-15 18:17:00 -04:00
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 : '<div (click)="onClick"></div>'
} )
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
} ) ;
2016-12-12 18:59:12 -05:00
2019-08-15 18:17:00 -04:00
// #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 : '<div *ngIf="something === undefined"></div>'
} )
export class AppComponent {
something = 'foo' ;
} ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2017-03-29 19:14:37 -04:00
2019-08-15 18:17:00 -04:00
// 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 : '<p> Using an invalid pipe {{data | dat}} </p>'
} )
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 ) ;
} ) ;
2017-11-22 15:09:49 -05:00
2019-08-15 18:17:00 -04:00
// 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 ( [ ] ) ;
} ) ;
2017-04-04 11:30:38 -04:00
2019-08-15 18:17:00 -04:00
// 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 : \ `
< form # f = "ngForm" ( ngSubmit ) = " onSubmit ( f ) " novalidate >
< input name = "first" ngModel required # first = "ngModel" >
< input name = "last" ngModel >
< button > Submit < / button >
< / form >
< p > First name value : { { first . value } } < / p >
< p > First name valid : { { first . valid } } < / p >
< p > Form value : { { f . value | json } } < / p >
< p > Form valid : { { f . valid } } < / p >
\ ` ,
} )
export class AppComponent {
onSubmit ( form : NgForm ) { }
} ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2017-04-04 14:37:06 -04:00
2019-08-15 18:17:00 -04:00
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 '@
} ) ;
2016-12-12 19:42:20 -05:00
2019-08-15 18:17:00 -04:00
// 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 : \ `
< div * ngIf = "comps | async; let comps; else loading" >
< / div >
< ng - template # loading > Loading comps . . . < / n g - t e m p l a t e >
\ `
} )
export class AppComponent { } ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2017-04-04 13:31:01 -04:00
2019-08-15 18:17:00 -04:00
// 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 : \ `
< div >
{ fieldCount , plural , = 0 { no fields } = 1 { 1 field } other { { { fieldCount } } fields } }
< / div >
\ `
} )
export class AppComponent {
fieldCount : number ;
} ` );
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const ngDiags = ngLS . getDiagnostics ( fileName ) ;
expect ( ngDiags ) . toEqual ( [ ] ) ;
} ) ;
2017-04-13 19:19:59 -04:00
2019-08-15 18:17:00 -04:00
// Issue #15885
it ( 'should be able to remove null and undefined from a type' , ( ) = > {
mockHost . overrideOptions ( options = > {
options . strictNullChecks = true ;
return options ;
2017-04-10 18:10:34 -04:00
} ) ;
2019-08-15 18:17:00 -04:00
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 ( [ ] ) ;
} ) ;
2017-04-10 18:10:34 -04:00
2019-08-15 18:17:00 -04:00
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 ;
2017-09-28 12:30:34 -04:00
} ) ;
2019-08-15 18:17:00 -04:00
const tsDiags = tsLS . getSemanticDiagnostics ( fileName ) ;
expect ( tsDiags ) . toEqual ( [ ] ) ;
const diagnostic = ngLS . getDiagnostics ( fileName ) ;
expect ( diagnostic ) . toEqual ( [ ] ) ;
} ) ;
2017-09-28 12:30:34 -04:00
2019-08-15 18:17:00 -04:00
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'. ` ) ;
2016-11-22 12:10:23 -05:00
} ) ;
2019-08-15 18:17:00 -04:00
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 ;
}
2016-11-22 12:10:23 -05:00
} ) ;