refactor(language-service): Differentiate Inline and External template (#32127)

This commit creates two concrete classes Inline and External
TemplateSource to differentiate between templates in TS file and
HTML file.
Knowing the template type makes the code much more explicit which
filetype we are dealing with.

With these two classes, there is no need for `getTemplateAt()` method in
TypeScriptHost. Removing this method is safe since it is not used in the
extension. This reduces the API surface of TypescriptHost.

PR Close #32127
This commit is contained in:
Keen Yee Liau 2019-08-13 15:38:37 -07:00 committed by Andrew Kushnir
parent 253a1125bf
commit 40b28742a9
7 changed files with 340 additions and 227 deletions

View File

@ -36,7 +36,7 @@ class LanguageServiceImpl implements LanguageService {
const results: Diagnostic[] = [];
const templates = this.host.getTemplates(fileName);
for (const template of templates) {
const ast = this.host.getTemplateAst(template, fileName);
const ast = this.host.getTemplateAst(template);
results.push(...getTemplateDiagnostics(template, ast));
}
const declarations = this.host.getDeclarations(fileName);

View File

@ -0,0 +1,168 @@
/**
* @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 {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli';
import * as ts from 'typescript';
import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host';
/**
* A base class to represent a template and which component class it is
* associated with. A template source could answer basic questions about
* top-level declarations of its class through the members() and query()
* methods.
*/
abstract class BaseTemplate implements ng.TemplateSource {
private readonly program: ts.Program;
private membersTable: ng.SymbolTable|undefined;
private queryCache: ng.SymbolQuery|undefined;
constructor(
private readonly host: TypeScriptServiceHost,
private readonly classDeclNode: ts.ClassDeclaration,
private readonly classSymbol: ng.StaticSymbol) {
this.program = host.program;
}
abstract get span(): ng.Span;
abstract get fileName(): string;
abstract get source(): string;
/**
* Return the Angular StaticSymbol for the class that contains this template.
*/
get type() { return this.classSymbol; }
/**
* Return a Map-like data structure that allows users to retrieve some or all
* top-level declarations in the associated component class.
*/
get members() {
if (!this.membersTable) {
const typeChecker = this.program.getTypeChecker();
const sourceFile = this.classDeclNode.getSourceFile();
this.membersTable =
getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode);
}
return this.membersTable;
}
/**
* Return an engine that provides more information about symbols in the
* template.
*/
get query() {
if (!this.queryCache) {
const program = this.program;
const typeChecker = program.getTypeChecker();
const sourceFile = this.classDeclNode.getSourceFile();
this.queryCache = getSymbolQuery(program, typeChecker, sourceFile, () => {
// Computing the ast is relatively expensive. Do it only when absolutely
// necessary.
// TODO: There is circular dependency here between TemplateSource and
// TypeScriptHost. Consider refactoring the code to break this cycle.
const ast = this.host.getTemplateAst(this);
return getPipesTable(sourceFile, program, typeChecker, ast.pipes || []);
});
}
return this.queryCache;
}
}
/**
* An InlineTemplate represents template defined in a TS file through the
* `template` attribute in the decorator.
*/
export class InlineTemplate extends BaseTemplate {
public readonly fileName: string;
public readonly source: string;
public readonly span: ng.Span;
constructor(
templateNode: ts.StringLiteralLike, classDeclNode: ts.ClassDeclaration,
classSymbol: ng.StaticSymbol, host: TypeScriptServiceHost) {
super(host, classDeclNode, classSymbol);
const sourceFile = templateNode.getSourceFile();
if (sourceFile !== classDeclNode.getSourceFile()) {
throw new Error(`Inline template and component class should belong to the same source file`);
}
this.fileName = sourceFile.fileName;
this.source = templateNode.text;
this.span = {
// TS string literal includes surrounding quotes in the start/end offsets.
start: templateNode.getStart() + 1,
end: templateNode.getEnd() - 1,
};
}
}
/**
* An ExternalTemplate represents template defined in an external (most likely
* HTML, but not necessarily) file through the `templateUrl` attribute in the
* decorator.
* Note that there is no ts.Node associated with the template because it's not
* a TS file.
*/
export class ExternalTemplate extends BaseTemplate {
public readonly span: ng.Span;
constructor(
public readonly source: string, public readonly fileName: string,
classDeclNode: ts.ClassDeclaration, classSymbol: ng.StaticSymbol,
host: TypeScriptServiceHost) {
super(host, classDeclNode, classSymbol);
this.span = {
start: 0,
end: source.length,
};
}
}
/**
* Given a template node, return the ClassDeclaration node that corresponds to
* the component class for the template.
*
* For example,
*
* @Component({
* template: '<div></div>' <-- template node
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param node template node
*/
export function getClassDeclFromTemplateNode(node: ts.Node): ts.ClassDeclaration|undefined {
if (!ts.isStringLiteralLike(node)) {
return;
}
if (!node.parent || !ts.isPropertyAssignment(node.parent)) {
return;
}
const propAsgnNode = node.parent;
if (propAsgnNode.name.getText() !== 'template') {
return;
}
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}

View File

@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
import {CompileDirectiveMetadata, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli/src/language_services';
import * as tss from 'typescript/lib/tsserverlibrary';
import {AstResult, TemplateInfo} from './common';
@ -20,6 +19,7 @@ export {
Pipes,
Signature,
Span,
StaticSymbol,
Symbol,
SymbolDeclaration,
SymbolQuery,
@ -40,16 +40,6 @@ export interface TemplateSource {
*/
readonly source: string;
/**
* The version of the source. As files are modified the version should change. That is, if the
* `LanguageService` requesting template information for a source file and that file has changed
* since the last time the host was asked for the file then this version string should be
* different. No assumptions are made about the format of this string.
*
* The version can change more often than the source but should not change less often.
*/
readonly version: string;
/**
* The span of the template within the source file.
*/
@ -69,6 +59,11 @@ export interface TemplateSource {
* A `SymbolQuery` for the context of the template.
*/
readonly query: SymbolQuery;
/**
* Name of the file that contains the template. Could be `.html` or `.ts`.
*/
readonly fileName: string;
}
/**
@ -80,7 +75,6 @@ export interface TemplateSource {
*/
export type TemplateSources = TemplateSource[] | undefined;
/**
* Error information found getting declaration information
*
@ -174,13 +168,6 @@ export type Declarations = Declaration[];
* @publicApi
*/
export interface LanguageServiceHost {
/**
* 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`
* is not in a template literal string then this method should return `undefined`.
*/
getTemplateAt(fileName: string, position: number): TemplateSource|undefined;
/**
* Return the template source information for all templates in `fileName` or for `fileName` if
* it is a template file.
@ -205,7 +192,7 @@ export interface LanguageServiceHost {
/**
* Return the AST for both HTML and template for the contextFile.
*/
getTemplateAst(template: TemplateSource, contextFile: string): AstResult;
getTemplateAst(template: TemplateSource): AstResult;
/**
* Return the template AST for the node that corresponds to the position.
@ -381,20 +368,20 @@ export interface LanguageService {
/**
* Returns a list of all error for all templates in the given file.
*/
getDiagnostics(fileName: string): tss.Diagnostic[];
getDiagnostics(fileName: string): ts.Diagnostic[];
/**
* Return the completions at the given position.
*/
getCompletionsAt(fileName: string, position: number): tss.CompletionInfo|undefined;
getCompletionsAt(fileName: string, position: number): ts.CompletionInfo|undefined;
/**
* Return the definition location for the symbol at position.
*/
getDefinitionAt(fileName: string, position: number): tss.DefinitionInfoAndBoundSpan|undefined;
getDefinitionAt(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan|undefined;
/**
* Return the hover information for the symbol at position.
*/
getHoverAt(fileName: string, position: number): tss.QuickInfo|undefined;
getHoverAt(fileName: string, position: number): ts.QuickInfo|undefined;
}

View File

@ -7,14 +7,15 @@
*/
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 {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import * as ts from 'typescript';
import {AstResult, TemplateInfo} from './common';
import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host';
import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, SymbolQuery, TemplateSource} from './types';
import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template';
import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTighestNode} from './utils';
/**
* Create a `LanguageServiceHost`
@ -119,30 +120,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
getTemplateReferences(): string[] { return [...this.templateReferences]; }
/**
* Get the Angular template in the file, if any. If TS file is provided then
* return the inline template, otherwise return the external template.
* @param fileName Either TS or HTML file
* @param position Only used if file is TS
*/
getTemplateAt(fileName: string, position: number): TemplateSource|undefined {
if (fileName.endsWith('.ts')) {
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
const node = this.findNode(sourceFile, position);
if (node) {
return this.getSourceFromNode(fileName, node);
}
}
} else {
const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) {
return this.getSourceFromType(fileName, componentSymbol);
}
}
return undefined;
}
/**
* Checks whether the program has changed and returns all analyzed modules.
* If program has changed, invalidate all caches and update fileToComponent
@ -183,30 +160,30 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return this.analyzedModules;
}
/**
* Find all templates in the specified `file`.
* @param fileName TS or HTML file
*/
getTemplates(fileName: string): TemplateSource[] {
const results: TemplateSource[] = [];
if (fileName.endsWith('.ts')) {
// Find every template string in the file
const visit = (child: ts.Node) => {
const templateSource = this.getSourceFromNode(fileName, child);
if (templateSource) {
results.push(templateSource);
const template = this.getInternalTemplate(child);
if (template) {
results.push(template);
} else {
ts.forEachChild(child, visit);
}
};
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
ts.forEachChild(sourceFile, visit);
}
} else {
const componentSymbol = this.fileToComponent.get(fileName);
if (componentSymbol) {
const templateSource = this.getTemplateAt(fileName, 0);
if (templateSource) {
results.push(templateSource);
}
const template = this.getExternalTemplate(fileName);
if (template) {
results.push(template);
}
}
return results;
@ -239,7 +216,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return this.program.getSourceFile(fileName);
}
private get program() {
get program() {
const program = this.tsLS.getProgram();
if (!program) {
// Program is very very unlikely to be undefined.
@ -288,87 +265,64 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
/**
* 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(
fileName: string, source: string, span: Span, classSymbol: StaticSymbol,
declaration: ts.ClassDeclaration, node: ts.Node, sourceFile: ts.SourceFile): TemplateSource
|undefined {
let queryCache: SymbolQuery|undefined = undefined;
const self = this;
const program = this.program;
const typeChecker = program.getTypeChecker();
if (declaration) {
return {
version: this.host.getScriptVersion(fileName),
source,
span,
type: classSymbol,
get members() {
return getClassMembersFromDeclaration(program, typeChecker, sourceFile, declaration);
},
get query() {
if (!queryCache) {
const templateInfo = self.getTemplateAst(this, fileName);
const pipes = templateInfo && templateInfo.pipes || [];
queryCache = getSymbolQuery(
program, typeChecker, sourceFile,
() => getPipesTable(sourceFile, program, typeChecker, pipes));
}
return queryCache;
}
};
}
}
/**
* Return the TemplateSource for the inline template.
* @param fileName TS file that contains the template
* Return the TemplateSource if `node` is a template node.
*
* For example,
*
* @Component({
* template: '<div></div>' <-- template node
* })
* class AppComponent {}
* ^---- class declaration node
*
*
* @param node Potential template node
*/
private getSourceFromNode(fileName: string, node: ts.Node): TemplateSource|undefined {
switch (node.kind) {
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.StringLiteral:
const [declaration] = this.getTemplateClassDeclFromNode(node);
if (declaration && declaration.name) {
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
return this.getSourceFromDeclaration(
fileName, this.stringOf(node) || '', shrink(spanOf(node)),
this.reflector.getStaticSymbol(sourceFile.fileName, declaration.name.text),
declaration, node, sourceFile);
}
}
break;
private getInternalTemplate(node: ts.Node): TemplateSource|undefined {
if (!ts.isStringLiteralLike(node)) {
return;
}
return;
const classDecl = getClassDeclFromTemplateNode(node);
if (!classDecl || !classDecl.name) { // Does not handle anonymous class
return;
}
const fileName = node.getSourceFile().fileName;
const classSymbol = this.reflector.getStaticSymbol(fileName, classDecl.name.text);
return new InlineTemplate(node, classDecl, classSymbol, this);
}
/**
* Return the TemplateSource for the template associated with the classSymbol.
* @param fileName Template file (HTML)
* @param classSymbol
* Return the external template for `fileName`.
* @param fileName HTML file
*/
private getSourceFromType(fileName: string, classSymbol: StaticSymbol): TemplateSource|undefined {
const declaration = this.getTemplateClassFromStaticSymbol(classSymbol);
if (declaration) {
const snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) {
const source = snapshot.getText(0, snapshot.getLength());
return this.getSourceFromDeclaration(
fileName, source, {start: 0, end: source.length}, classSymbol, declaration, declaration,
declaration.getSourceFile());
}
private getExternalTemplate(fileName: string): TemplateSource|undefined {
// First get the text for the template
const snapshot = this.host.getScriptSnapshot(fileName);
if (!snapshot) {
return;
}
return;
const source = snapshot.getText(0, snapshot.getLength());
// Next find the component class symbol
const classSymbol = this.fileToComponent.get(fileName);
if (!classSymbol) {
return;
}
// Then use the class symbol to find the actual ts.ClassDeclaration node
const sourceFile = this.getSourceFile(classSymbol.filePath);
if (!sourceFile) {
return;
}
// TODO: This only considers top-level class declarations in a source file.
// This would not find a class declaration in a namespace, for example.
const classDecl = sourceFile.forEachChild((child) => {
if (ts.isClassDeclaration(child) && child.name && child.name.text === classSymbol.name) {
return child;
}
});
if (!classDecl) {
return;
}
return new ExternalTemplate(source, fileName, classDecl, classSymbol, this);
}
private collectError(error: any, filePath: string|null) {
@ -393,68 +347,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return this._reflector;
}
private getTemplateClassFromStaticSymbol(type: StaticSymbol): ts.ClassDeclaration|undefined {
const source = this.getSourceFile(type.filePath);
if (!source) {
return;
}
const declarationNode = ts.forEachChild(source, child => {
if (child.kind === ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = child as ts.ClassDeclaration;
if (classDeclaration.name && classDeclaration.name.text === type.name) {
return classDeclaration;
}
}
});
return declarationNode as ts.ClassDeclaration;
}
private static missingTemplate: [ts.ClassDeclaration | undefined, ts.Expression|undefined] =
[undefined, undefined];
/**
* Given a template string node, see if it is an Angular template string, and if so return the
* containing class.
*/
private getTemplateClassDeclFromNode(currentToken: ts.Node):
[ts.ClassDeclaration | undefined, ts.Expression|undefined] {
// Verify we are in a 'template' property assignment, in an object literal, which is an call
// arg, in a decorator
let parentNode = currentToken.parent; // PropertyAssignment
if (!parentNode) {
return TypeScriptServiceHost.missingTemplate;
}
if (parentNode.kind !== ts.SyntaxKind.PropertyAssignment) {
return TypeScriptServiceHost.missingTemplate;
} else {
// TODO: Is this different for a literal, i.e. a quoted property name like "template"?
if ((parentNode as any).name.text !== 'template') {
return TypeScriptServiceHost.missingTemplate;
}
}
parentNode = parentNode.parent; // ObjectLiteralExpression
if (!parentNode || parentNode.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
return TypeScriptServiceHost.missingTemplate;
}
parentNode = parentNode.parent; // CallExpression
if (!parentNode || parentNode.kind !== ts.SyntaxKind.CallExpression) {
return TypeScriptServiceHost.missingTemplate;
}
const callTarget = (<ts.CallExpression>parentNode).expression;
const decorator = parentNode.parent; // Decorator
if (!decorator || decorator.kind !== ts.SyntaxKind.Decorator) {
return TypeScriptServiceHost.missingTemplate;
}
const declaration = <ts.ClassDeclaration>decorator.parent; // ClassDeclaration
if (!declaration || declaration.kind !== ts.SyntaxKind.ClassDeclaration) {
return TypeScriptServiceHost.missingTemplate;
}
return [declaration, callTarget];
}
private getCollectedErrors(defaultSpan: Span, sourceFile: ts.SourceFile): DeclarationError[] {
const errors = this.collectedErrors.get(sourceFile.fileName);
return (errors && errors.map((e: any) => {
@ -512,31 +404,31 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
}
private stringOf(node: ts.Node): string|undefined {
switch (node.kind) {
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
return (<ts.LiteralExpression>node).text;
case ts.SyntaxKind.StringLiteral:
return (<ts.StringLiteral>node).text;
}
}
private findNode(sourceFile: ts.SourceFile, position: number): ts.Node|undefined {
function find(node: ts.Node): ts.Node|undefined {
if (position >= node.getStart() && position < node.getEnd()) {
return ts.forEachChild(node, find) || node;
}
}
return find(sourceFile);
}
/**
* Return the parsed template for the template at the specified `position`.
* @param fileName TS or HTML file
* @param position Position of the template in the TS file, otherwise ignored.
*/
getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined {
const template = this.getTemplateAt(fileName, position);
let template: TemplateSource|undefined;
if (fileName.endsWith('.ts')) {
const sourceFile = this.getSourceFile(fileName);
if (!sourceFile) {
return;
}
// Find the node that most closely matches the position
const node = findTighestNode(sourceFile, position);
if (!node) {
return;
}
template = this.getInternalTemplate(node);
} else {
template = this.getExternalTemplate(fileName);
}
if (!template) {
return;
}
const astResult = this.getTemplateAst(template, fileName);
const astResult = this.getTemplateAst(template);
if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive &&
astResult.directives && astResult.pipes && astResult.expressionParser) {
return {
@ -553,7 +445,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
}
}
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
getTemplateAst(template: TemplateSource): AstResult {
try {
const resolvedMetadata = this.resolver.getNonNormalizedDirectiveMetadata(template.type);
const metadata = resolvedMetadata && resolvedMetadata.metadata;
@ -590,8 +482,8 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
parseErrors: parseResult.errors, expressionParser, errors
};
} catch (e) {
const span =
e.fileName === contextFile && template.query.getSpanAt(e.line, e.column) || template.span;
const span = e.fileName === template.fileName && template.query.getSpanAt(e.line, e.column) ||
template.span;
return {
errors: [{
kind: DiagnosticKind.Error,
@ -619,11 +511,6 @@ function spanOf(node: ts.Node): Span {
return {start: node.getStart(), end: node.getEnd()};
}
function shrink(span: Span, offset?: number) {
if (offset == null) offset = 1;
return {start: span.start + offset, end: span.end - offset};
}
function spanAt(sourceFile: ts.SourceFile, line: number, column: number): Span|undefined {
if (line != null && column != null) {
const position = ts.getPositionOfLineAndCharacter(sourceFile, line, column);

View File

@ -177,3 +177,14 @@ export function findTemplateAstAt(
return new AstPath<TemplateAst>(path, position);
}
/**
* Return the node that most tightly encompass the specified `position`.
* @param node
* @param position
*/
export function findTighestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTighestNode(c, position)) || node;
}
}

View File

@ -0,0 +1,31 @@
/**
* @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 * as ts from 'typescript';
import {getClassDeclFromTemplateNode} from '../src/template';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('getClassDeclFromTemplateNode', () => {
it('should return class declaration', () => {
const host = new MockTypescriptHost(['/app/app.component.ts'], toh);
const tsLS = ts.createLanguageService(host);
const sourceFile = tsLS.getProgram() !.getSourceFile('/app/app.component.ts');
expect(sourceFile).toBeTruthy();
const classDecl = sourceFile !.forEachChild(function visit(node): ts.Node | undefined {
const candidate = getClassDeclFromTemplateNode(node);
if (candidate) {
return candidate;
}
return node.forEachChild(visit);
});
expect(classDecl).toBeTruthy();
expect(ts.isClassDeclaration(classDecl !)).toBe(true);
expect((classDecl as ts.ClassDeclaration).name !.text).toBe('AppComponent');
});
});

View File

@ -66,4 +66,33 @@ describe('TypeScriptServiceHost', () => {
ngLSHost.getSourceFile('/src/test.ng');
}).toThrowError('Non-TS source file requested: /src/test.ng');
});
it('should be able to find a single inline template', () => {
const tsLSHost = new MockTypescriptHost(['/app/app.component.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const templates = ngLSHost.getTemplates('/app/app.component.ts');
expect(templates.length).toBe(1);
const template = templates[0];
expect(template.source).toContain('<h2>{{hero.name}} details!</h2>');
});
it('should be able to find multiple inline templates', () => {
const tsLSHost = new MockTypescriptHost(['/app/parsing-cases.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const templates = ngLSHost.getTemplates('/app/parsing-cases.ts');
expect(templates.length).toBe(16);
});
it('should be able to find external template', () => {
const tsLSHost = new MockTypescriptHost(['/app/main.ts'], toh);
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
ngLSHost.getAnalyzedModules();
const templates = ngLSHost.getTemplates('/app/test.ng');
expect(templates.length).toBe(1);
const template = templates[0];
expect(template.source).toContain('<h2>{{hero.name}} details!</h2>');
});
});