refactor(compiler-cli): move the expression expression type checker (#16562)
The expression type checker moved from the language service to the compiler-cli in preparation to using it to check template expressions.
This commit is contained in:

committed by
Jason Aden

parent
9e661e58d1
commit
bb0902c592
255
packages/compiler-cli/test/diagnostics/mocks.ts
Normal file
255
packages/compiler-cli/test/diagnostics/mocks.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @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 {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticAndDynamicReflectionCapabilities, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler';
|
||||
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
|
||||
import {CompilerHostContext} from 'compiler-cli';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DiagnosticTemplateInfo} from '../../src/diagnostics/expression_diagnostics';
|
||||
import {getClassFromStaticSymbol, getClassMembers, getPipesTable, getSymbolQuery} from '../../src/diagnostics/typescript_symbols';
|
||||
import {Directory, MockAotContext} from '../mocks';
|
||||
|
||||
const packages = path.join(__dirname, '../../../../../packages');
|
||||
|
||||
const realFiles = new Map<string, string>();
|
||||
|
||||
export class MockLanguageServiceHost implements ts.LanguageServiceHost, CompilerHostContext {
|
||||
private options: ts.CompilerOptions;
|
||||
private context: MockAotContext;
|
||||
private assumedExist = new Set<string>();
|
||||
|
||||
constructor(private scripts: string[], files: Directory, currentDirectory: string = '/') {
|
||||
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/*': [packages + '/*']}
|
||||
};
|
||||
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.dirname(ts.getDefaultLibFilePath(this.getCompilationSettings()));
|
||||
fileName = path.join(libPath, basename);
|
||||
}
|
||||
if (fileName.startsWith('app/')) {
|
||||
fileName = path.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; },
|
||||
getOutputFileName(sourceFilePath: string) { return sourceFilePath; }
|
||||
},
|
||||
staticSymbolCache);
|
||||
|
||||
export class DiagnosticContext {
|
||||
_analyzedModules: NgAnalyzedModules;
|
||||
_staticSymbolResolver: StaticSymbolResolver|undefined;
|
||||
_reflector: StaticReflector|undefined;
|
||||
_errors: {e: any, path?: string}[] = [];
|
||||
_resolver: CompileMetadataResolver|undefined;
|
||||
_refletor: StaticReflector;
|
||||
|
||||
constructor(
|
||||
public service: ts.LanguageService, public program: ts.Program,
|
||||
public checker: ts.TypeChecker, public host: AotCompilerHost) {}
|
||||
|
||||
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 !));
|
||||
StaticAndDynamicReflectionCapabilities.install(result);
|
||||
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(
|
||||
source: string, url: string, parseExpansionForms: boolean = false,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
|
||||
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, 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 programSymbols = extractProgramSymbols(
|
||||
this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName),
|
||||
analyzeHost);
|
||||
|
||||
analyzedModules = this._analyzedModules =
|
||||
analyzeNgModules(programSymbols, analyzeHost, 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, expressionParser, new DomElementSchemaRegistry(), htmlParser, null !, []);
|
||||
const htmlResult = htmlParser.parse(template, '', 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);
|
||||
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[];
|
||||
}
|
Reference in New Issue
Block a user