2017-05-09 19:16:50 -04: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
|
|
|
|
*/
|
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
import {analyzeNgModules, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, createOfflineCompileUrlResolver, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, Parser, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, TemplateParser} from '@angular/compiler';
|
2019-11-13 17:26:58 -05:00
|
|
|
import {Directory, MockAotContext} from '@angular/compiler-cli/test/mocks';
|
|
|
|
import {setup} from '@angular/compiler-cli/test/test_support';
|
2017-05-09 19:16:50 -04:00
|
|
|
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
|
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as path from 'path';
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2019-11-13 17:26:58 -05:00
|
|
|
import {DiagnosticTemplateInfo} from '../src/expression_diagnostics';
|
|
|
|
import {getClassMembers, getPipesTable, getSymbolQuery} from '../src/typescript_symbols';
|
2017-05-09 19:16:50 -04:00
|
|
|
|
|
|
|
const realFiles = new Map<string, string>();
|
|
|
|
|
2017-10-20 12:46:41 -04:00
|
|
|
export class MockLanguageServiceHost implements ts.LanguageServiceHost {
|
2017-05-09 19:16:50 -04:00
|
|
|
private options: ts.CompilerOptions;
|
|
|
|
private context: MockAotContext;
|
|
|
|
private assumedExist = new Set<string>();
|
|
|
|
|
|
|
|
constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') {
|
2019-01-25 13:44:49 -05:00
|
|
|
const support = setup();
|
|
|
|
|
2017-05-09 19:16:50 -04: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 13:44:49 -05:00
|
|
|
paths: {'@angular/*': [path.join(support.basePath, 'node_modules/@angular/*')]}
|
2017-05-09 19:16:50 -04:00
|
|
|
};
|
2017-06-09 17:50:57 -04:00
|
|
|
this.context = new MockAotContext(currentDirectory, files);
|
2017-05-09 19:16:50 -04:00
|
|
|
}
|
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
getCompilationSettings(): ts.CompilerOptions {
|
|
|
|
return this.options;
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
getScriptFileNames(): string[] {
|
|
|
|
return this.scripts;
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
getScriptVersion(fileName: string): string {
|
|
|
|
return '0';
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
|
|
|
getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined {
|
|
|
|
const content = this.internalReadFile(fileName);
|
|
|
|
if (content) {
|
|
|
|
return ts.ScriptSnapshot.fromString(content);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
getCurrentDirectory(): string {
|
|
|
|
return this.context.currentDirectory;
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
|
|
|
return 'lib.d.ts';
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
readFile(fileName: string): string {
|
|
|
|
return this.internalReadFile(fileName) as string;
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
readResource(fileName: string): Promise<string> {
|
|
|
|
return Promise.resolve('');
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
assumeFileExists(fileName: string): void {
|
|
|
|
this.assumedExist.add(fileName);
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
|
|
|
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 13:50:08 -05:00
|
|
|
let libPath = path.posix.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings()));
|
|
|
|
fileName = path.posix.join(libPath, basename);
|
2017-05-09 19:16:50 -04:00
|
|
|
}
|
|
|
|
if (fileName.startsWith('app/')) {
|
2019-01-25 13:50:08 -05:00
|
|
|
fileName = path.posix.join(this.context.currentDirectory, fileName);
|
2017-05-09 19:16:50 -04: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(
|
|
|
|
{
|
2020-04-03 23:57:39 -04:00
|
|
|
loadSummary(filePath: string) {
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
isSourceFile(sourceFilePath: string) {
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
toSummaryFileName(sourceFilePath: string) {
|
|
|
|
return sourceFilePath;
|
|
|
|
},
|
|
|
|
fromSummaryFileName(filePath: string): string {
|
|
|
|
return filePath;
|
|
|
|
},
|
2017-05-09 19:16:50 -04:00
|
|
|
},
|
|
|
|
staticSymbolCache);
|
|
|
|
|
|
|
|
export class DiagnosticContext {
|
2020-01-15 01:48:13 -05:00
|
|
|
private _analyzedModules: NgAnalyzedModules|undefined;
|
|
|
|
private _staticSymbolResolver: StaticSymbolResolver|undefined;
|
|
|
|
private _reflector: StaticReflector|undefined;
|
|
|
|
private _errors: {e: any, path?: string}[] = [];
|
|
|
|
private _resolver: CompileMetadataResolver|undefined;
|
2017-05-09 19:16:50 -04:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
public service: ts.LanguageService, public program: ts.Program,
|
2017-10-20 12:46:41 -04:00
|
|
|
public checker: ts.TypeChecker, public host: StaticSymbolResolverHost) {}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
private collectError(e: any, path?: string) {
|
|
|
|
this._errors.push({e, path});
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
|
|
|
|
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(
|
2020-04-03 23:57:39 -04:00
|
|
|
summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath!));
|
2017-05-09 19:16:50 -04:00
|
|
|
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 {
|
2020-04-03 23:57:39 -04:00
|
|
|
get(url: string): Promise<string> {
|
|
|
|
return Promise.resolve('');
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
};
|
|
|
|
const urlResolver = createOfflineCompileUrlResolver();
|
|
|
|
const htmlParser = new class extends HtmlParser {
|
2020-04-03 23:57:39 -04:00
|
|
|
parse(): ParseTreeResult {
|
|
|
|
return new ParseTreeResult([], []);
|
|
|
|
}
|
2017-05-09 19:16:50 -04: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 12:40:28 -04: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 19:16:50 -04:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
get analyzedModules(): NgAnalyzedModules {
|
|
|
|
let analyzedModules = this._analyzedModules;
|
|
|
|
if (!analyzedModules) {
|
2020-04-03 23:57:39 -04:00
|
|
|
const analyzeHost = {
|
|
|
|
isSourceFile(filePath: string) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
2017-09-12 12:40:28 -04:00
|
|
|
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
|
2017-05-09 19:16:50 -04:00
|
|
|
analyzedModules = this._analyzedModules =
|
2017-09-12 12:40:28 -04:00
|
|
|
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
|
2017-05-09 19:16:50 -04: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 16:46:51 -04:00
|
|
|
config, context.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser,
|
2020-04-03 23:57:39 -04:00
|
|
|
null!, []);
|
2019-02-08 17:10:19 -05:00
|
|
|
const htmlResult = htmlParser.parse(template, '', {tokenizeExpansionForms: true});
|
2017-05-09 19:16:50 -04: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,
|
2020-04-03 23:57:39 -04:00
|
|
|
directive: metadata,
|
|
|
|
directives,
|
|
|
|
pipes,
|
|
|
|
parseErrors: parseResult.errors,
|
|
|
|
expressionParser
|
2017-05-09 19:16:50 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 11:59:25 -05:00
|
|
|
if (sourceFile) {
|
|
|
|
const query = getSymbolQuery(
|
|
|
|
context.program, context.checker, sourceFile,
|
|
|
|
() => getPipesTable(
|
|
|
|
sourceFile, context.program, context.checker, compiledTemplate.pipes));
|
|
|
|
return {
|
|
|
|
fileName: templateFile,
|
2020-04-03 23:57:39 -04:00
|
|
|
offset: 0,
|
|
|
|
query,
|
|
|
|
members,
|
2018-02-08 11:59:25 -05:00
|
|
|
htmlAst: compiledTemplate.htmlAst,
|
2020-02-09 15:29:45 -05:00
|
|
|
templateAst: compiledTemplate.templateAst,
|
|
|
|
source: sourceFile.text,
|
2018-02-08 11:59:25 -05:00
|
|
|
};
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 23:57:39 -04:00
|
|
|
function removeMissing<T>(values: (T|null|undefined)[]): T[] {
|
2017-05-09 19:16:50 -04:00
|
|
|
return values.filter(e => !!e) as T[];
|
|
|
|
}
|