| 
									
										
										
										
											2017-05-09 16:16:50 -07: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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-25 19:44:49 +01:00
										 |  |  | import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler'; | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  | import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; | 
					
						
							|  |  |  | import * as fs from 'fs'; | 
					
						
							|  |  |  | import * as path from 'path'; | 
					
						
							|  |  |  | import * as ts from 'typescript'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {DiagnosticTemplateInfo} from '../../src/diagnostics/expression_diagnostics'; | 
					
						
							| 
									
										
										
										
											2019-01-25 19:44:49 +01:00
										 |  |  | import {getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols'; | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  | import {Directory, MockAotContext} from '../mocks'; | 
					
						
							| 
									
										
										
										
											2019-01-25 19:44:49 +01:00
										 |  |  | import {setup} from '../test_support'; | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | const realFiles = new Map<string, string>(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-20 09:46:41 -07:00
										 |  |  | export class MockLanguageServiceHost implements ts.LanguageServiceHost { | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |   private options: ts.CompilerOptions; | 
					
						
							|  |  |  |   private context: MockAotContext; | 
					
						
							|  |  |  |   private assumedExist = new Set<string>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') { | 
					
						
							| 
									
										
										
										
											2019-01-25 19:44:49 +01:00
										 |  |  |     const support = setup(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     this.options = { | 
					
						
							|  |  |  |       target: ts.ScriptTarget.ES5, | 
					
						
							|  |  |  |       module: ts.ModuleKind.CommonJS, | 
					
						
							|  |  |  |       moduleResolution: ts.ModuleResolutionKind.NodeJs, | 
					
						
							|  |  |  |       emitDecoratorMetadata: true, | 
					
						
							|  |  |  |       experimentalDecorators: true, | 
					
						
							|  |  |  |       removeComments: false, | 
					
						
							|  |  |  |       noImplicitAny: false, | 
					
						
							|  |  |  |       skipLibCheck: true, | 
					
						
							|  |  |  |       skipDefaultLibCheck: true, | 
					
						
							|  |  |  |       strictNullChecks: true, | 
					
						
							|  |  |  |       baseUrl: currentDirectory, | 
					
						
							|  |  |  |       lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'], | 
					
						
							| 
									
										
										
										
											2019-01-25 19:44:49 +01:00
										 |  |  |       paths: {'@angular/*': [path.join(support.basePath, 'node_modules/@angular/*')]} | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2017-06-09 14:50:57 -07:00
										 |  |  |     this.context = new MockAotContext(currentDirectory, files); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getCompilationSettings(): ts.CompilerOptions { return this.options; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getScriptFileNames(): string[] { return this.scripts; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getScriptVersion(fileName: string): string { return '0'; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined { | 
					
						
							|  |  |  |     const content = this.internalReadFile(fileName); | 
					
						
							|  |  |  |     if (content) { | 
					
						
							|  |  |  |       return ts.ScriptSnapshot.fromString(content); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getCurrentDirectory(): string { return this.context.currentDirectory; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   readFile(fileName: string): string { return this.internalReadFile(fileName) as string; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   readResource(fileName: string): Promise<string> { return Promise.resolve(''); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   assumeFileExists(fileName: string): void { this.assumedExist.add(fileName); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   fileExists(fileName: string): boolean { | 
					
						
							|  |  |  |     return this.assumedExist.has(fileName) || this.internalReadFile(fileName) != null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private internalReadFile(fileName: string): string|undefined { | 
					
						
							|  |  |  |     let basename = path.basename(fileName); | 
					
						
							|  |  |  |     if (/^lib.*\.d\.ts$/.test(basename)) { | 
					
						
							| 
									
										
										
										
											2019-01-25 19:50:08 +01:00
										 |  |  |       let libPath = path.posix.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings())); | 
					
						
							|  |  |  |       fileName = path.posix.join(libPath, basename); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |     if (fileName.startsWith('app/')) { | 
					
						
							| 
									
										
										
										
											2019-01-25 19:50:08 +01:00
										 |  |  |       fileName = path.posix.join(this.context.currentDirectory, fileName); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |     if (this.context.fileExists(fileName)) { | 
					
						
							|  |  |  |       return this.context.readFile(fileName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (realFiles.has(fileName)) { | 
					
						
							|  |  |  |       return realFiles.get(fileName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (fs.existsSync(fileName)) { | 
					
						
							|  |  |  |       const content = fs.readFileSync(fileName, 'utf8'); | 
					
						
							|  |  |  |       realFiles.set(fileName, content); | 
					
						
							|  |  |  |       return content; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return undefined; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const staticSymbolCache = new StaticSymbolCache(); | 
					
						
							|  |  |  | const summaryResolver = new AotSummaryResolver( | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       loadSummary(filePath: string) { return null; }, | 
					
						
							|  |  |  |       isSourceFile(sourceFilePath: string) { return true; }, | 
					
						
							| 
									
										
										
										
											2017-08-15 14:41:48 -07:00
										 |  |  |       toSummaryFileName(sourceFilePath: string) { return sourceFilePath; }, | 
					
						
							|  |  |  |       fromSummaryFileName(filePath: string): string{return filePath;}, | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     staticSymbolCache); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export class DiagnosticContext { | 
					
						
							| 
									
										
										
										
											2017-07-27 16:13:16 -07:00
										 |  |  |   // tslint:disable
 | 
					
						
							| 
									
										
										
										
											2018-06-18 16:38:33 -07:00
										 |  |  |   // TODO(issue/24571): remove '!'.
 | 
					
						
							|  |  |  |   _analyzedModules !: NgAnalyzedModules; | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |   _staticSymbolResolver: StaticSymbolResolver|undefined; | 
					
						
							|  |  |  |   _reflector: StaticReflector|undefined; | 
					
						
							|  |  |  |   _errors: {e: any, path?: string}[] = []; | 
					
						
							|  |  |  |   _resolver: CompileMetadataResolver|undefined; | 
					
						
							| 
									
										
										
										
											2018-06-18 16:38:33 -07:00
										 |  |  |   // TODO(issue/24571): remove '!'.
 | 
					
						
							|  |  |  |   _refletor !: StaticReflector; | 
					
						
							| 
									
										
										
										
											2017-07-27 16:13:16 -07:00
										 |  |  |   // tslint:enable
 | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   constructor( | 
					
						
							|  |  |  |       public service: ts.LanguageService, public program: ts.Program, | 
					
						
							| 
									
										
										
										
											2017-10-20 09:46:41 -07:00
										 |  |  |       public checker: ts.TypeChecker, public host: StaticSymbolResolverHost) {} | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   private collectError(e: any, path?: string) { this._errors.push({e, path}); } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private get staticSymbolResolver(): StaticSymbolResolver { | 
					
						
							|  |  |  |     let result = this._staticSymbolResolver; | 
					
						
							|  |  |  |     if (!result) { | 
					
						
							|  |  |  |       result = this._staticSymbolResolver = new StaticSymbolResolver( | 
					
						
							|  |  |  |           this.host, staticSymbolCache, summaryResolver, | 
					
						
							|  |  |  |           (e, filePath) => this.collectError(e, filePath)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return result; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   get reflector(): StaticReflector { | 
					
						
							|  |  |  |     if (!this._reflector) { | 
					
						
							|  |  |  |       const ssr = this.staticSymbolResolver; | 
					
						
							|  |  |  |       const result = this._reflector = new StaticReflector( | 
					
						
							|  |  |  |           summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !)); | 
					
						
							|  |  |  |       this._reflector = result; | 
					
						
							|  |  |  |       return result; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return this._reflector; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   get resolver(): CompileMetadataResolver { | 
					
						
							|  |  |  |     let result = this._resolver; | 
					
						
							|  |  |  |     if (!result) { | 
					
						
							|  |  |  |       const moduleResolver = new NgModuleResolver(this.reflector); | 
					
						
							|  |  |  |       const directiveResolver = new DirectiveResolver(this.reflector); | 
					
						
							|  |  |  |       const pipeResolver = new PipeResolver(this.reflector); | 
					
						
							|  |  |  |       const elementSchemaRegistry = new DomElementSchemaRegistry(); | 
					
						
							|  |  |  |       const resourceLoader = new class extends ResourceLoader { | 
					
						
							|  |  |  |         get(url: string): Promise<string> { return Promise.resolve(''); } | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       const urlResolver = createOfflineCompileUrlResolver(); | 
					
						
							|  |  |  |       const htmlParser = new class extends HtmlParser { | 
					
						
							| 
									
										
										
										
											2019-02-08 22:10:19 +00:00
										 |  |  |         parse(): ParseTreeResult { return new ParseTreeResult([], []); } | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // This tracks the CompileConfig in codegen.ts. Currently these options
 | 
					
						
							|  |  |  |       // are hard-coded.
 | 
					
						
							|  |  |  |       const config = | 
					
						
							|  |  |  |           new CompilerConfig({defaultEncapsulation: ViewEncapsulation.Emulated, useJit: false}); | 
					
						
							|  |  |  |       const directiveNormalizer = | 
					
						
							|  |  |  |           new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       result = this._resolver = new CompileMetadataResolver( | 
					
						
							| 
									
										
										
										
											2017-09-12 09:40:28 -07:00
										 |  |  |           config, htmlParser, moduleResolver, directiveResolver, pipeResolver, | 
					
						
							|  |  |  |           new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(), | 
					
						
							|  |  |  |           staticSymbolCache, this.reflector, | 
					
						
							|  |  |  |           (error, type) => this.collectError(error, type && type.filePath)); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |     return result; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   get analyzedModules(): NgAnalyzedModules { | 
					
						
							|  |  |  |     let analyzedModules = this._analyzedModules; | 
					
						
							|  |  |  |     if (!analyzedModules) { | 
					
						
							|  |  |  |       const analyzeHost = {isSourceFile(filePath: string) { return true; }}; | 
					
						
							| 
									
										
										
										
											2017-09-12 09:40:28 -07:00
										 |  |  |       const programFiles = this.program.getSourceFiles().map(sf => sf.fileName); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |       analyzedModules = this._analyzedModules = | 
					
						
							| 
									
										
										
										
											2017-09-12 09:40:28 -07:00
										 |  |  |           analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |     return analyzedModules; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   getStaticSymbol(path: string, name: string): StaticSymbol { | 
					
						
							|  |  |  |     return staticSymbolCache.get(path, name); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function compileTemplate(context: DiagnosticContext, type: StaticSymbol, template: string) { | 
					
						
							|  |  |  |   // Compiler the template string.
 | 
					
						
							|  |  |  |   const resolvedMetadata = context.resolver.getNonNormalizedDirectiveMetadata(type); | 
					
						
							|  |  |  |   const metadata = resolvedMetadata && resolvedMetadata.metadata; | 
					
						
							|  |  |  |   if (metadata) { | 
					
						
							|  |  |  |     const rawHtmlParser = new HtmlParser(); | 
					
						
							|  |  |  |     const htmlParser = new I18NHtmlParser(rawHtmlParser); | 
					
						
							|  |  |  |     const expressionParser = new Parser(new Lexer()); | 
					
						
							|  |  |  |     const config = new CompilerConfig(); | 
					
						
							|  |  |  |     const parser = new TemplateParser( | 
					
						
							| 
									
										
										
										
											2017-05-18 13:46:51 -07:00
										 |  |  |         config, context.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser, | 
					
						
							|  |  |  |         null !, []); | 
					
						
							| 
									
										
										
										
											2019-02-08 22:10:19 +00:00
										 |  |  |     const htmlResult = htmlParser.parse(template, '', {tokenizeExpansionForms: true}); | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     const analyzedModules = context.analyzedModules; | 
					
						
							|  |  |  |     // let errors: Diagnostic[]|undefined = undefined;
 | 
					
						
							|  |  |  |     let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(type); | 
					
						
							|  |  |  |     if (ngModule) { | 
					
						
							|  |  |  |       const resolvedDirectives = ngModule.transitiveModule.directives.map( | 
					
						
							|  |  |  |           d => context.resolver.getNonNormalizedDirectiveMetadata(d.reference)); | 
					
						
							|  |  |  |       const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary()); | 
					
						
							|  |  |  |       const pipes = ngModule.transitiveModule.pipes.map( | 
					
						
							|  |  |  |           p => context.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); | 
					
						
							|  |  |  |       const schemas = ngModule.schemas; | 
					
						
							|  |  |  |       const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas); | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         htmlAst: htmlResult.rootNodes, | 
					
						
							|  |  |  |         templateAst: parseResult.templateAst, | 
					
						
							|  |  |  |         directive: metadata, directives, pipes, | 
					
						
							|  |  |  |         parseErrors: parseResult.errors, expressionParser | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function getDiagnosticTemplateInfo( | 
					
						
							|  |  |  |     context: DiagnosticContext, type: StaticSymbol, templateFile: string, | 
					
						
							|  |  |  |     template: string): DiagnosticTemplateInfo|undefined { | 
					
						
							|  |  |  |   const compiledTemplate = compileTemplate(context, type, template); | 
					
						
							|  |  |  |   if (compiledTemplate && compiledTemplate.templateAst) { | 
					
						
							|  |  |  |     const members = getClassMembers(context.program, context.checker, type); | 
					
						
							|  |  |  |     if (members) { | 
					
						
							|  |  |  |       const sourceFile = context.program.getSourceFile(type.filePath); | 
					
						
							| 
									
										
										
										
											2018-02-08 08:59:25 -08:00
										 |  |  |       if (sourceFile) { | 
					
						
							|  |  |  |         const query = getSymbolQuery( | 
					
						
							|  |  |  |             context.program, context.checker, sourceFile, | 
					
						
							|  |  |  |             () => getPipesTable( | 
					
						
							|  |  |  |                 sourceFile, context.program, context.checker, compiledTemplate.pipes)); | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |           fileName: templateFile, | 
					
						
							|  |  |  |           offset: 0, query, members, | 
					
						
							|  |  |  |           htmlAst: compiledTemplate.htmlAst, | 
					
						
							|  |  |  |           templateAst: compiledTemplate.templateAst | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2017-05-09 16:16:50 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function removeMissing<T>(values: (T | null | undefined)[]): T[] { | 
					
						
							|  |  |  |   return values.filter(e => !!e) as T[]; | 
					
						
							|  |  |  | } |