refactor(language-service): Cleanup TypescriptHost (#32017)

Cleanup the logic in TypeScriptHost as to when langauge service state
should be synchronized with the editor state.

The model employed follows that of tsserver, in which case it is the
caller's responsiblity to synchronize host data before any LS methods
are called.

PR Close #32017
This commit is contained in:
Keen Yee Liau 2019-08-05 19:37:30 -07:00 committed by Andrew Kushnir
parent 7a75f7805c
commit 9808d91c62
6 changed files with 202 additions and 233 deletions

View File

@ -29,20 +29,22 @@ export function createLanguageService(host: TypeScriptServiceHost): LanguageServ
class LanguageServiceImpl implements LanguageService { class LanguageServiceImpl implements LanguageService {
constructor(private readonly host: TypeScriptServiceHost) {} constructor(private readonly host: TypeScriptServiceHost) {}
getTemplateReferences(): string[] { return this.host.getTemplateReferences(); } getTemplateReferences(): string[] {
this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
return this.host.getTemplateReferences();
}
getDiagnostics(fileName: string): tss.Diagnostic[] { getDiagnostics(fileName: string): tss.Diagnostic[] {
const analyzedModules = this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const results: Diagnostic[] = []; const results: Diagnostic[] = [];
const templates = this.host.getTemplates(fileName); const templates = this.host.getTemplates(fileName);
for (const template of templates) { for (const template of templates) {
const ast = this.host.getTemplateAst(template, fileName); const ast = this.host.getTemplateAst(template, fileName);
results.push(...getTemplateDiagnostics(template, ast)); results.push(...getTemplateDiagnostics(template, ast));
} }
const declarations = this.host.getDeclarations(fileName); const declarations = this.host.getDeclarations(fileName);
if (declarations && declarations.length) { if (declarations && declarations.length) {
const summary = this.host.getAnalyzedModules(); results.push(...getDeclarationDiagnostics(declarations, analyzedModules));
results.push(...getDeclarationDiagnostics(declarations, summary));
} }
if (!results.length) { if (!results.length) {
return []; return [];
@ -52,7 +54,8 @@ class LanguageServiceImpl implements LanguageService {
} }
getPipesAt(fileName: string, position: number): CompilePipeSummary[] { getPipesAt(fileName: string, position: number): CompilePipeSummary[] {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return templateInfo.pipes; return templateInfo.pipes;
} }
@ -60,21 +63,24 @@ class LanguageServiceImpl implements LanguageService {
} }
getCompletionsAt(fileName: string, position: number): Completion[]|undefined { getCompletionsAt(fileName: string, position: number): Completion[]|undefined {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return getTemplateCompletions(templateInfo); return getTemplateCompletions(templateInfo);
} }
} }
getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined { getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return getDefinitionAndBoundSpan(templateInfo); return getDefinitionAndBoundSpan(templateInfo);
} }
} }
getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined { getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined {
let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); this.host.getAnalyzedModules(); // same role as 'synchronizeHostData'
const templateInfo = this.host.getTemplateAstAtPosition(fileName, position);
if (templateInfo) { if (templateInfo) {
return getHover(templateInfo); return getHover(templateInfo);
} }

View File

@ -18,6 +18,7 @@ const projectHostMap = new WeakMap<tss.server.Project, TypeScriptServiceHost>();
export function getExternalFiles(project: tss.server.Project): string[]|undefined { export function getExternalFiles(project: tss.server.Project): string[]|undefined {
const host = projectHostMap.get(project); const host = projectHostMap.get(project);
if (host) { if (host) {
host.getAnalyzedModules();
const externalFiles = host.getTemplateReferences(); const externalFiles = host.getTemplateReferences();
return externalFiles; return externalFiles;
} }

View File

@ -174,11 +174,6 @@ export type Declarations = Declaration[];
* @publicApi * @publicApi
*/ */
export interface LanguageServiceHost { export interface LanguageServiceHost {
/**
* The resolver to use to find compiler metadata.
*/
readonly resolver: CompileMetadataResolver;
/** /**
* Returns the template information for templates in `fileName` at the given location. If * Returns the template information for templates in `fileName` at the given location. If
* `fileName` refers to a template file then the `position` should be ignored. If the `position` * `fileName` refers to a template file then the `position` should be ignored. If the `position`

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AotSummaryResolver, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; import {AotSummaryResolver, CompileMetadataResolver, CompileNgModuleMetadata, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler';
import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services';
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -14,9 +14,7 @@ import * as ts from 'typescript';
import {AstResult, TemplateInfo} from './common'; import {AstResult, TemplateInfo} from './common';
import {createLanguageService} from './language_service'; import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host'; import {ReflectorHost} from './reflector_host';
import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, Symbol, SymbolQuery, TemplateSource, TemplateSources} from './types'; import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, SymbolQuery, TemplateSource} from './types';
/** /**
* Create a `LanguageServiceHost` * Create a `LanguageServiceHost`
@ -54,38 +52,46 @@ export class DummyResourceLoader extends ResourceLoader {
* @publicApi * @publicApi
*/ */
export class TypeScriptServiceHost implements LanguageServiceHost { export class TypeScriptServiceHost implements LanguageServiceHost {
// TODO(issue/24571): remove '!'. private readonly summaryResolver: AotSummaryResolver;
private _resolver !: CompileMetadataResolver | null; private readonly reflectorHost: ReflectorHost;
private _staticSymbolCache = new StaticSymbolCache(); private readonly staticSymbolResolver: StaticSymbolResolver;
// TODO(issue/24571): remove '!'.
private _summaryResolver !: AotSummaryResolver;
// TODO(issue/24571): remove '!'.
private _staticSymbolResolver !: StaticSymbolResolver;
// TODO(issue/24571): remove '!'.
private _reflector !: StaticReflector | null;
// TODO(issue/24571): remove '!'.
private _reflectorHost !: ReflectorHost;
// TODO(issue/24571): remove '!'.
private _checker !: ts.TypeChecker | null;
private lastProgram: ts.Program|undefined;
private modulesOutOfDate: boolean = true;
// TODO(issue/24571): remove '!'.
private analyzedModules !: NgAnalyzedModules | null;
private fileToComponent = new Map<string, StaticSymbol>();
// TODO(issue/24571): remove '!'.
private templateReferences !: string[] | null;
private collectedErrors = new Map<string, any[]>();
private fileVersions = new Map<string, string>();
constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {} private readonly staticSymbolCache = new StaticSymbolCache();
private readonly fileToComponent = new Map<string, StaticSymbol>();
private readonly collectedErrors = new Map<string, any[]>();
private readonly fileVersions = new Map<string, string>();
/** private lastProgram: ts.Program|undefined = undefined;
* Angular LanguageServiceHost implementation private templateReferences: string[] = [];
*/ private analyzedModules: NgAnalyzedModules = {
get resolver(): CompileMetadataResolver { files: [],
this.validate(); ngModuleByPipeOrDirective: new Map(),
let result = this._resolver; ngModules: [],
if (!result) { };
// Data members below are prefixed with '_' because they have corresponding
// getters. These properties get invalidated when caches are cleared.
private _resolver: CompileMetadataResolver|null = null;
private _reflector: StaticReflector|null = null;
constructor(
private readonly host: ts.LanguageServiceHost, private readonly tsLS: ts.LanguageService) {
this.summaryResolver = new AotSummaryResolver(
{
loadSummary(filePath: string) { return null; },
isSourceFile(sourceFilePath: string) { return true; },
toSummaryFileName(sourceFilePath: string) { return sourceFilePath; },
fromSummaryFileName(filePath: string): string{return filePath;},
},
this.staticSymbolCache);
this.reflectorHost = new ReflectorHost(() => tsLS.getProgram() !, host);
this.staticSymbolResolver = new StaticSymbolResolver(
this.reflectorHost, this.staticSymbolCache, this.summaryResolver,
(e, filePath) => this.collectError(e, filePath !));
}
private get resolver(): CompileMetadataResolver {
if (!this._resolver) {
const moduleResolver = new NgModuleResolver(this.reflector); const moduleResolver = new NgModuleResolver(this.reflector);
const directiveResolver = new DirectiveResolver(this.reflector); const directiveResolver = new DirectiveResolver(this.reflector);
const pipeResolver = new PipeResolver(this.reflector); const pipeResolver = new PipeResolver(this.reflector);
@ -95,24 +101,23 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
const htmlParser = new DummyHtmlParser(); const htmlParser = new DummyHtmlParser();
// This tracks the CompileConfig in codegen.ts. Currently these options // This tracks the CompileConfig in codegen.ts. Currently these options
// are hard-coded. // are hard-coded.
const config = const config = new CompilerConfig({
new CompilerConfig({defaultEncapsulation: ViewEncapsulation.Emulated, useJit: false}); defaultEncapsulation: ViewEncapsulation.Emulated,
useJit: false,
});
const directiveNormalizer = const directiveNormalizer =
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config); new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver( this._resolver = new CompileMetadataResolver(
config, htmlParser, moduleResolver, directiveResolver, pipeResolver, config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(), new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
this._staticSymbolCache, this.reflector, this.staticSymbolCache, this.reflector,
(error, type) => this.collectError(error, type && type.filePath)); (error, type) => this.collectError(error, type && type.filePath));
} }
return result; return this._resolver;
} }
getTemplateReferences(): string[] { getTemplateReferences(): string[] { return [...this.templateReferences]; }
this.ensureTemplateMap();
return this.templateReferences || [];
}
/** /**
* Get the Angular template in the file, if any. If TS file is provided then * Get the Angular template in the file, if any. If TS file is provided then
@ -126,54 +131,64 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (sourceFile) { if (sourceFile) {
const node = this.findNode(sourceFile, position); const node = this.findNode(sourceFile, position);
if (node) { if (node) {
return this.getSourceFromNode( return this.getSourceFromNode(fileName, node);
fileName, this.host.getScriptVersion(sourceFile.fileName), node);
} }
} }
} else { } else {
this.ensureTemplateMap();
const componentSymbol = this.fileToComponent.get(fileName); const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) { if (componentSymbol) {
return this.getSourceFromType( return this.getSourceFromType(fileName, componentSymbol);
fileName, this.host.getScriptVersion(fileName), componentSymbol);
} }
} }
return undefined; return undefined;
} }
/**
* Checks whether the program has changed and returns all analyzed modules.
* If program has changed, invalidate all caches and update fileToComponent
* and templateReferences.
* In addition to returning information about NgModules, this method plays the
* same role as 'synchronizeHostData' in tsserver.
*/
getAnalyzedModules(): NgAnalyzedModules { getAnalyzedModules(): NgAnalyzedModules {
this.updateAnalyzedModules(); if (this.upToDate()) {
return this.ensureAnalyzedModules(); return this.analyzedModules;
} }
private ensureAnalyzedModules(): NgAnalyzedModules { // Invalidate caches
let analyzedModules = this.analyzedModules; this.templateReferences = [];
if (!analyzedModules) { this.fileToComponent.clear();
if (this.host.getScriptFileNames().length === 0) { this.collectedErrors.clear();
analyzedModules = {
files: [],
ngModuleByPipeOrDirective: new Map(),
ngModules: [],
};
} else {
const analyzeHost = {isSourceFile(filePath: string) { return true; }}; const analyzeHost = {isSourceFile(filePath: string) { return true; }};
const programFiles = this.program !.getSourceFiles().map(sf => sf.fileName); const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this.analyzedModules =
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver); analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
// update template references and fileToComponent
const urlResolver = createOfflineCompileUrlResolver();
for (const ngModule of this.analyzedModules.ngModules) {
for (const directive of ngModule.declaredDirectives) {
const {metadata} = this.resolver.getNonNormalizedDirectiveMetadata(directive.reference) !;
if (metadata.isComponent && metadata.template && metadata.template.templateUrl) {
const templateName = urlResolver.resolve(
this.reflector.componentModuleUrl(directive.reference),
metadata.template.templateUrl);
this.fileToComponent.set(templateName, directive.reference);
this.templateReferences.push(templateName);
} }
this.analyzedModules = analyzedModules;
} }
return analyzedModules; }
return this.analyzedModules;
} }
getTemplates(fileName: string): TemplateSource[] { getTemplates(fileName: string): TemplateSource[] {
const results: TemplateSource[] = []; const results: TemplateSource[] = [];
if (fileName.endsWith('.ts')) { if (fileName.endsWith('.ts')) {
let version = this.host.getScriptVersion(fileName); // Find every template string in the file
const visit = (child: ts.Node) => {
// Find each template string in the file const templateSource = this.getSourceFromNode(fileName, child);
let visit = (child: ts.Node) => {
let templateSource = this.getSourceFromNode(fileName, version, child);
if (templateSource) { if (templateSource) {
results.push(templateSource); results.push(templateSource);
} else { } else {
@ -181,12 +196,11 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
}; };
let sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
ts.forEachChild(sourceFile, visit); ts.forEachChild(sourceFile, visit);
} }
} else { } else {
this.ensureTemplateMap();
const componentSymbol = this.fileToComponent.get(fileName); const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) { if (componentSymbol) {
const templateSource = this.getTemplateAt(fileName, 0); const templateSource = this.getTemplateAt(fileName, 0);
@ -222,116 +236,91 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (!fileName.endsWith('.ts')) { if (!fileName.endsWith('.ts')) {
throw new Error(`Non-TS source file requested: ${fileName}`); throw new Error(`Non-TS source file requested: ${fileName}`);
} }
return this.tsService.getProgram() !.getSourceFile(fileName); return this.program.getSourceFile(fileName);
} }
updateAnalyzedModules() { private get program() {
this.validate(); const program = this.tsLS.getProgram();
if (this.modulesOutOfDate) { if (!program) {
this.analyzedModules = null; // Program is very very unlikely to be undefined.
this._reflector = null; throw new Error('No program in language service!');
this.templateReferences = null;
this.fileToComponent.clear();
this.ensureAnalyzedModules();
this.modulesOutOfDate = false;
} }
return program;
} }
private get program() { return this.tsService.getProgram(); } /**
* Checks whether the program has changed, and invalidate caches if it has.
private get checker() { * Returns true if modules are up-to-date, false otherwise.
let checker = this._checker; * This should only be called by getAnalyzedModules().
if (!checker) { */
checker = this._checker = this.program !.getTypeChecker(); private upToDate() {
}
return checker;
}
private validate() {
const program = this.program; const program = this.program;
if (this.lastProgram !== program) { if (this.lastProgram === program) {
return true;
}
this._resolver = null;
this._reflector = null;
// Invalidate file that have changed in the static symbol resolver // Invalidate file that have changed in the static symbol resolver
const invalidateFile = (fileName: string) =>
this._staticSymbolResolver.invalidateFile(fileName);
this.clearCaches();
const seen = new Set<string>(); const seen = new Set<string>();
for (let sourceFile of this.program !.getSourceFiles()) { for (const sourceFile of program.getSourceFiles()) {
const fileName = sourceFile.fileName; const fileName = sourceFile.fileName;
seen.add(fileName); seen.add(fileName);
const version = this.host.getScriptVersion(fileName); const version = this.host.getScriptVersion(fileName);
const lastVersion = this.fileVersions.get(fileName); const lastVersion = this.fileVersions.get(fileName);
if (version != lastVersion) { if (version !== lastVersion) {
this.fileVersions.set(fileName, version); this.fileVersions.set(fileName, version);
if (this._staticSymbolResolver) { this.staticSymbolResolver.invalidateFile(fileName);
invalidateFile(fileName);
}
} }
} }
// Remove file versions that are no longer in the file and invalidate them. // Remove file versions that are no longer in the file and invalidate them.
const missing = Array.from(this.fileVersions.keys()).filter(f => !seen.has(f)); const missing = Array.from(this.fileVersions.keys()).filter(f => !seen.has(f));
missing.forEach(f => this.fileVersions.delete(f)); missing.forEach(f => {
if (this._staticSymbolResolver) { this.fileVersions.delete(f);
missing.forEach(invalidateFile); this.staticSymbolResolver.invalidateFile(f);
} });
this.lastProgram = program; this.lastProgram = program;
}
} return false;
private clearCaches() {
this._checker = null;
this._resolver = null;
this.collectedErrors.clear();
this.modulesOutOfDate = true;
}
private ensureTemplateMap() {
if (!this.templateReferences) {
const templateReference: string[] = [];
const ngModuleSummary = this.getAnalyzedModules();
const urlResolver = createOfflineCompileUrlResolver();
for (const module of ngModuleSummary.ngModules) {
for (const directive of module.declaredDirectives) {
const {metadata} = this.resolver.getNonNormalizedDirectiveMetadata(directive.reference) !;
if (metadata.isComponent && metadata.template && metadata.template.templateUrl) {
const templateName = urlResolver.resolve(
this.reflector.componentModuleUrl(directive.reference),
metadata.template.templateUrl);
this.fileToComponent.set(templateName, directive.reference);
templateReference.push(templateName);
}
}
}
this.templateReferences = templateReference;
}
} }
/**
* Return the template source given the Class declaration node for the template.
* @param fileName Name of the file that contains the template. Could be TS or HTML.
* @param source Source text of the template.
* @param span Source span of the template.
* @param classSymbol Angular symbol for the class declaration.
* @param declaration TypeScript symbol for the class declaration.
* @param node If file is TS this is the template node, otherwise it's the class declaration node.
* @param sourceFile Source file of the class declaration.
*/
private getSourceFromDeclaration( private getSourceFromDeclaration(
fileName: string, version: string, source: string, span: Span, type: StaticSymbol, fileName: string, source: string, span: Span, classSymbol: StaticSymbol,
declaration: ts.ClassDeclaration, node: ts.Node, sourceFile: ts.SourceFile): TemplateSource declaration: ts.ClassDeclaration, node: ts.Node, sourceFile: ts.SourceFile): TemplateSource
|undefined { |undefined {
let queryCache: SymbolQuery|undefined = undefined; let queryCache: SymbolQuery|undefined = undefined;
const t = this; const self = this;
const program = this.program;
const typeChecker = program.getTypeChecker();
if (declaration) { if (declaration) {
return { return {
version, version: this.host.getScriptVersion(fileName),
source, source,
span, span,
type, type: classSymbol,
get members() { get members() {
return getClassMembersFromDeclaration(t.program !, t.checker, sourceFile, declaration); return getClassMembersFromDeclaration(program, typeChecker, sourceFile, declaration);
}, },
get query() { get query() {
if (!queryCache) { if (!queryCache) {
let pipes: CompilePipeSummary[] = []; const templateInfo = self.getTemplateAst(this, fileName);
const templateInfo = t.getTemplateAstAtPosition(fileName, node.getStart()); const pipes = templateInfo && templateInfo.pipes || [];
if (templateInfo) {
pipes = templateInfo.pipes;
}
queryCache = getSymbolQuery( queryCache = getSymbolQuery(
t.program !, t.checker, sourceFile, program, typeChecker, sourceFile,
() => getPipesTable(sourceFile, t.program !, t.checker, pipes)); () => getPipesTable(sourceFile, program, typeChecker, pipes));
} }
return queryCache; return queryCache;
} }
@ -339,49 +328,47 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
} }
private getSourceFromNode(fileName: string, version: string, node: ts.Node): TemplateSource /**
|undefined { * Return the TemplateSource for the inline template.
let result: TemplateSource|undefined = undefined; * @param fileName TS file that contains the template
const t = this; * @param node Potential template node
*/
private getSourceFromNode(fileName: string, node: ts.Node): TemplateSource|undefined {
switch (node.kind) { switch (node.kind) {
case ts.SyntaxKind.NoSubstitutionTemplateLiteral: case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.StringLiteral:
let [declaration, decorator] = this.getTemplateClassDeclFromNode(node); const [declaration] = this.getTemplateClassDeclFromNode(node);
if (declaration && declaration.name) { if (declaration && declaration.name) {
const sourceFile = this.getSourceFile(fileName); const sourceFile = this.getSourceFile(fileName);
if (sourceFile) { if (sourceFile) {
return this.getSourceFromDeclaration( return this.getSourceFromDeclaration(
fileName, version, this.stringOf(node) || '', shrink(spanOf(node)), fileName, this.stringOf(node) || '', shrink(spanOf(node)),
this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text), this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text),
declaration, node, sourceFile); declaration, node, sourceFile);
} }
} }
break; break;
} }
return result; return;
} }
private getSourceFromType(fileName: string, version: string, type: StaticSymbol): TemplateSource /**
|undefined { * Return the TemplateSource for the template associated with the classSymbol.
let result: TemplateSource|undefined = undefined; * @param fileName Template file (HTML)
const declaration = this.getTemplateClassFromStaticSymbol(type); * @param classSymbol
*/
private getSourceFromType(fileName: string, classSymbol: StaticSymbol): TemplateSource|undefined {
const declaration = this.getTemplateClassFromStaticSymbol(classSymbol);
if (declaration) { if (declaration) {
const snapshot = this.host.getScriptSnapshot(fileName); const snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) { if (snapshot) {
const source = snapshot.getText(0, snapshot.getLength()); const source = snapshot.getText(0, snapshot.getLength());
result = this.getSourceFromDeclaration( return this.getSourceFromDeclaration(
fileName, version, source, {start: 0, end: source.length}, type, declaration, fileName, source, {start: 0, end: source.length}, classSymbol, declaration, declaration,
declaration, declaration.getSourceFile()); declaration.getSourceFile());
} }
} }
return result; return;
}
private get reflectorHost(): ReflectorHost {
if (!this._reflectorHost) {
this._reflectorHost = new ReflectorHost(() => this.tsService.getProgram() !, this.host);
}
return this._reflectorHost;
} }
private collectError(error: any, filePath: string|null) { private collectError(error: any, filePath: string|null) {
@ -395,41 +382,25 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
} }
} }
private get staticSymbolResolver(): StaticSymbolResolver {
let result = this._staticSymbolResolver;
if (!result) {
this._summaryResolver = new AotSummaryResolver(
{
loadSummary(filePath: string) { return null; },
isSourceFile(sourceFilePath: string) { return true; },
toSummaryFileName(sourceFilePath: string) { return sourceFilePath; },
fromSummaryFileName(filePath: string): string{return filePath;},
},
this._staticSymbolCache);
result = this._staticSymbolResolver = new StaticSymbolResolver(
this.reflectorHost as any, this._staticSymbolCache, this._summaryResolver,
(e, filePath) => this.collectError(e, filePath !));
}
return result;
}
private get reflector(): StaticReflector { private get reflector(): StaticReflector {
let result = this._reflector; let result = this._reflector;
if (!result) { if (!result) {
const ssr = this.staticSymbolResolver; const ssr = this.staticSymbolResolver;
result = this._reflector = new StaticReflector( result = this._reflector = new StaticReflector(
this._summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !)); this.summaryResolver, ssr, [], [], (e, filePath) => this.collectError(e, filePath !));
} }
return result; return result;
} }
private getTemplateClassFromStaticSymbol(type: StaticSymbol): ts.ClassDeclaration|undefined { private getTemplateClassFromStaticSymbol(type: StaticSymbol): ts.ClassDeclaration|undefined {
const source = this.getSourceFile(type.filePath); const source = this.getSourceFile(type.filePath);
if (source) { if (!source) {
return;
}
const declarationNode = ts.forEachChild(source, child => { const declarationNode = ts.forEachChild(source, child => {
if (child.kind === ts.SyntaxKind.ClassDeclaration) { if (child.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = child as ts.ClassDeclaration; const classDeclaration = child as ts.ClassDeclaration;
if (classDeclaration.name != null && classDeclaration.name.text === type.name) { if (classDeclaration.name && classDeclaration.name.text === type.name) {
return classDeclaration; return classDeclaration;
} }
} }
@ -437,9 +408,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return declarationNode as ts.ClassDeclaration; return declarationNode as ts.ClassDeclaration;
} }
return undefined;
}
private static missingTemplate: [ts.ClassDeclaration | undefined, ts.Expression|undefined] = private static missingTemplate: [ts.ClassDeclaration | undefined, ts.Expression|undefined] =
[undefined, undefined]; [undefined, undefined];
@ -509,7 +477,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
if (classDeclaration.name) { if (classDeclaration.name) {
const call = decorator.expression as ts.CallExpression; const call = decorator.expression as ts.CallExpression;
const target = call.expression; const target = call.expression;
const type = this.checker.getTypeAtLocation(target); const type = this.program.getTypeChecker().getTypeAtLocation(target);
if (type) { if (type) {
const staticSymbol = const staticSymbol =
this.reflector.getStaticSymbol(sourceFile.fileName, classDeclaration.name.text); this.reflector.getStaticSymbol(sourceFile.fileName, classDeclaration.name.text);
@ -598,12 +566,11 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
config, this.resolver.getReflector(), expressionParser, new DomElementSchemaRegistry(), config, this.resolver.getReflector(), expressionParser, new DomElementSchemaRegistry(),
htmlParser, null !, []); htmlParser, null !, []);
const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true});
const analyzedModules = this.getAnalyzedModules();
let errors: Diagnostic[]|undefined = undefined; let errors: Diagnostic[]|undefined = undefined;
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type); let ngModule = this.analyzedModules.ngModuleByPipeOrDirective.get(template.type);
if (!ngModule) { if (!ngModule) {
// Reported by the the declaration diagnostics. // Reported by the the declaration diagnostics.
ngModule = findSuitableDefaultModule(analyzedModules); ngModule = findSuitableDefaultModule(this.analyzedModules);
} }
if (ngModule) { if (ngModule) {
const directives = const directives =

View File

@ -151,7 +151,7 @@ export class MyComponent {
it('should hot crash with an incomplete class', () => { it('should hot crash with an incomplete class', () => {
expect(() => { expect(() => {
addCode('\nexport class', fileName => { ngHost.updateAnalyzedModules(); }); addCode('\nexport class', fileName => { ngHost.getAnalyzedModules(); });
}).not.toThrow(); }).not.toThrow();
}); });
@ -180,7 +180,7 @@ export class MyComponent {
tree: Node; tree: Node;
} }
`); `);
ngHost.updateAnalyzedModules(); ngHost.getAnalyzedModules();
contains('/app/my.component.ts', 'tree', 'children'); contains('/app/my.component.ts', 'tree', 'children');
}); });
@ -210,7 +210,7 @@ export class MyComponent {
const originalContent = mockHost.getFileContent(fileName); const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code; const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code); mockHost.override(fileName, originalContent + code);
ngHost.updateAnalyzedModules(); ngHost.getAnalyzedModules();
try { try {
cb(fileName, newContent); cb(fileName, newContent);
} finally { } finally {

View File

@ -371,7 +371,7 @@ describe('diagnostics', () => {
const originalContent = mockHost.getFileContent(fileName); const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code; const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code); mockHost.override(fileName, originalContent + code);
ngHost.updateAnalyzedModules(); ngHost.getAnalyzedModules();
try { try {
cb(fileName, newContent); cb(fileName, newContent);
} finally { } finally {