angular-cn/packages/language-service/src/template.ts

192 lines
6.4 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 * as ts from 'typescript';
import {createGlobalSymbolTable} from './global_symbols';
import * as ng from './types';
import {TypeScriptServiceHost} from './typescript_host';
import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './typescript_symbols';
/**
* 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 = this.query.mergeSymbolTable([
createGlobalSymbolTable(this.query),
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);
const pipes = (ast && ast.pipes) || [];
return getPipesTable(sourceFile, program, typeChecker, 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;
// node.text returns the TS internal representation of the normalized text,
// and all CR characters are stripped. node.getText() returns the raw text.
this.source = templateNode.getText().slice(1, -1); // strip leading and trailing quotes
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,
};
}
}
/**
* Returns a property assignment from the assignment value, or `undefined` if there is no
* assignment.
*/
export function getPropertyAssignmentFromValue(value: ts.Node): ts.PropertyAssignment|undefined {
if (!value.parent || !ts.isPropertyAssignment(value.parent)) {
return;
}
return value.parent;
}
/**
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
* directive class the property applies to.
* If the property assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
* })
* class AppComponent {}
* ^---- class declaration node
*
* @param propAsgn property assignment
*/
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
ts.ClassDeclaration|undefined {
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;
}
/**
* Determines if a property assignment is on a class decorator.
* See `getClassDeclFromDecoratorProperty`, which gets the class the decorator is applied to, for
* more details.
*
* @param prop property assignment
*/
export function isClassDecoratorProperty(propAsgn: ts.PropertyAssignment): boolean {
return !!getClassDeclFromDecoratorProp(propAsgn);
}