2019-08-13 18:38:37 -04:00
|
|
|
/**
|
|
|
|
* @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';
|
2019-08-21 17:36:00 -04:00
|
|
|
|
2019-10-17 21:42:27 -04:00
|
|
|
import {createGlobalSymbolTable} from './global_symbols';
|
2019-08-13 18:38:37 -04:00
|
|
|
import * as ng from './types';
|
|
|
|
import {TypeScriptServiceHost} from './typescript_host';
|
2019-11-13 17:26:58 -05:00
|
|
|
import {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './typescript_symbols';
|
|
|
|
|
2019-08-13 18:38:37 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2019-10-17 21:42:27 -04:00
|
|
|
this.membersTable = this.query.mergeSymbolTable([
|
|
|
|
createGlobalSymbolTable(this.query),
|
|
|
|
getClassMembersFromDeclaration(this.program, typeChecker, sourceFile, this.classDeclNode),
|
|
|
|
]);
|
2019-08-13 18:38:37 -04:00
|
|
|
}
|
|
|
|
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);
|
2019-11-27 18:46:58 -05:00
|
|
|
const pipes = (ast && ast.pipes) || [];
|
2019-08-21 17:36:00 -04:00
|
|
|
return getPipesTable(sourceFile, program, typeChecker, pipes);
|
2019-08-13 18:38:37 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
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;
|
2020-01-10 20:22:59 -05:00
|
|
|
// 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
|
2019-08-13 18:38:37 -04:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-21 21:46:30 -04:00
|
|
|
* 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.
|
2019-08-13 18:38:37 -04:00
|
|
|
*
|
|
|
|
* For example,
|
|
|
|
*
|
|
|
|
* @Component({
|
2019-08-21 21:46:30 -04:00
|
|
|
* template: '<div></div>'
|
|
|
|
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
|
2019-08-13 18:38:37 -04:00
|
|
|
* })
|
|
|
|
* class AppComponent {}
|
|
|
|
* ^---- class declaration node
|
|
|
|
*
|
2019-08-21 21:46:30 -04:00
|
|
|
* @param propAsgn property assignment
|
2019-08-13 18:38:37 -04:00
|
|
|
*/
|
2019-08-21 21:46:30 -04:00
|
|
|
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
|
|
|
|
ts.ClassDeclaration|undefined {
|
2019-08-13 18:38:37 -04:00
|
|
|
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;
|
|
|
|
}
|
2019-08-21 21:46:30 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|