This commit consolidates the options that can modify the parsing of text (e.g. HTML, Angular templates, CSS, i18n) into an AST for further processing into a single `options` hash. This makes the code cleaner and more readable, but also enables us to support further options to parsing without triggering wide ranging changes to code that should not be affected by these new options. Specifically, it will let us pass information about the placement of a template that is being parsed in its containing file, which is essential for accurate SourceMap processing. PR Close #28055
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
/**
|
|
* @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 {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';
|
|
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';
|
|
import {getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols';
|
|
import {Directory, MockAotContext} from '../mocks';
|
|
import {setup} from '../test_support';
|
|
|
|
const realFiles = new Map<string, string>();
|
|
|
|
export class MockLanguageServiceHost implements ts.LanguageServiceHost {
|
|
private options: ts.CompilerOptions;
|
|
private context: MockAotContext;
|
|
private assumedExist = new Set<string>();
|
|
|
|
constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') {
|
|
const support = setup();
|
|
|
|
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'],
|
|
paths: {'@angular/*': [path.join(support.basePath, 'node_modules/@angular/*')]}
|
|
};
|
|
this.context = new MockAotContext(currentDirectory, files);
|
|
}
|
|
|
|
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)) {
|
|
let libPath = path.posix.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings()));
|
|
fileName = path.posix.join(libPath, basename);
|
|
}
|
|
if (fileName.startsWith('app/')) {
|
|
fileName = path.posix.join(this.context.currentDirectory, fileName);
|
|
}
|
|
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; },
|
|
toSummaryFileName(sourceFilePath: string) { return sourceFilePath; },
|
|
fromSummaryFileName(filePath: string): string{return filePath;},
|
|
},
|
|
staticSymbolCache);
|
|
|
|
export class DiagnosticContext {
|
|
// tslint:disable
|
|
// TODO(issue/24571): remove '!'.
|
|
_analyzedModules !: NgAnalyzedModules;
|
|
_staticSymbolResolver: StaticSymbolResolver|undefined;
|
|
_reflector: StaticReflector|undefined;
|
|
_errors: {e: any, path?: string}[] = [];
|
|
_resolver: CompileMetadataResolver|undefined;
|
|
// TODO(issue/24571): remove '!'.
|
|
_refletor !: StaticReflector;
|
|
// tslint:enable
|
|
|
|
constructor(
|
|
public service: ts.LanguageService, public program: ts.Program,
|
|
public checker: ts.TypeChecker, public host: StaticSymbolResolverHost) {}
|
|
|
|
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 {
|
|
parse(): ParseTreeResult { return new ParseTreeResult([], []); }
|
|
};
|
|
|
|
// 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(
|
|
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
|
|
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
|
|
staticSymbolCache, this.reflector,
|
|
(error, type) => this.collectError(error, type && type.filePath));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
get analyzedModules(): NgAnalyzedModules {
|
|
let analyzedModules = this._analyzedModules;
|
|
if (!analyzedModules) {
|
|
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
|
|
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
|
|
analyzedModules = this._analyzedModules =
|
|
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
|
|
}
|
|
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(
|
|
config, context.reflector, expressionParser, new DomElementSchemaRegistry(), htmlParser,
|
|
null !, []);
|
|
const htmlResult = htmlParser.parse(template, '', {tokenizeExpansionForms: true});
|
|
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);
|
|
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
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeMissing<T>(values: (T | null | undefined)[]): T[] {
|
|
return values.filter(e => !!e) as T[];
|
|
}
|