Ayaz Hafiz 11f7e275e6 feat(compiler): type TemplateAst values as ASTWithSource (#35892)
TemplateAst values are currently typed as the base class AST, but they
are actually constructed with ASTWithSource. Type them as such, because
ASTWithSource gives more information about the consumed expression AST
to downstream customers (namely, the expression AST source).

Unblocks #35271

PR Close #35892
2020-03-09 12:14:09 -04:00

386 lines
13 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 {AstPath} from '../ast_path';
import {CompileDirectiveSummary, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata';
import {SecurityContext} from '../core';
import {ASTWithSource, BindingType, BoundElementProperty, ParsedEvent, ParsedEventType, ParsedVariable} from '../expression_parser/ast';
import {LifecycleHooks} from '../lifecycle_reflector';
import {ParseSourceSpan} from '../parse_util';
/**
* An Abstract Syntax Tree node representing part of a parsed Angular template.
*/
export interface TemplateAst {
/**
* The source span from which this node was parsed.
*/
sourceSpan: ParseSourceSpan;
/**
* Visit this node and possibly transform it.
*/
visit(visitor: TemplateAstVisitor, context: any): any;
}
/**
* A segment of text within the template.
*/
export class TextAst implements TemplateAst {
constructor(
public value: string, public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
/**
* A bound expression within the text of a template.
*/
export class BoundTextAst implements TemplateAst {
constructor(
public value: ASTWithSource, public ngContentIndex: number,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
}
/**
* A plain attribute on an element.
*/
export class AttrAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export const enum PropertyBindingType {
// A normal binding to a property (e.g. `[property]="expression"`).
Property,
// A binding to an element attribute (e.g. `[attr.name]="expression"`).
Attribute,
// A binding to a CSS class (e.g. `[class.name]="condition"`).
Class,
// A binding to a style rule (e.g. `[style.rule]="expression"`).
Style,
// A binding to an animation reference (e.g. `[animate.key]="expression"`).
Animation,
}
const BoundPropertyMapping = {
[BindingType.Animation]: PropertyBindingType.Animation,
[BindingType.Attribute]: PropertyBindingType.Attribute,
[BindingType.Class]: PropertyBindingType.Class,
[BindingType.Property]: PropertyBindingType.Property,
[BindingType.Style]: PropertyBindingType.Style,
};
/**
* A binding for an element property (e.g. `[property]="expression"`) or an animation trigger (e.g.
* `[@trigger]="stateExp"`)
*/
export class BoundElementPropertyAst implements TemplateAst {
readonly isAnimation: boolean;
constructor(
public name: string, public type: PropertyBindingType,
public securityContext: SecurityContext, public value: ASTWithSource,
public unit: string|null, public sourceSpan: ParseSourceSpan) {
this.isAnimation = this.type === PropertyBindingType.Animation;
}
static fromBoundProperty(prop: BoundElementProperty) {
const type = BoundPropertyMapping[prop.type];
return new BoundElementPropertyAst(
prop.name, type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan);
}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}
}
/**
* A binding for an element event (e.g. `(event)="handler()"`) or an animation trigger event (e.g.
* `(@trigger.phase)="callback($event)"`).
*/
export class BoundEventAst implements TemplateAst {
readonly fullName: string;
readonly isAnimation: boolean;
constructor(
public name: string, public target: string|null, public phase: string|null,
public handler: ASTWithSource, public sourceSpan: ParseSourceSpan,
public handlerSpan: ParseSourceSpan) {
this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase);
this.isAnimation = !!this.phase;
}
static calcFullName(name: string, target: string|null, phase: string|null): string {
if (target) {
return `${target}:${name}`;
}
if (phase) {
return `@${name}.${phase}`;
}
return name;
}
static fromParsedEvent(event: ParsedEvent) {
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
const phase: string|null =
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
return new BoundEventAst(
event.name, target, phase, event.handler, event.sourceSpan, event.handlerSpan);
}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEvent(this, context);
}
}
/**
* A reference declaration on an element (e.g. `let someName="expression"`).
*/
export class ReferenceAst implements TemplateAst {
constructor(
public name: string, public value: CompileTokenMetadata, public originalValue: string,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitReference(this, context);
}
}
/**
* A variable declaration on a <ng-template> (e.g. `var-someName="someLocalName"`).
*/
export class VariableAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
static fromParsedVariable(v: ParsedVariable) {
return new VariableAst(v.name, v.value, v.sourceSpan);
}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitVariable(this, context);
}
}
/**
* An element declaration in a template.
*/
export class ElementAst implements TemplateAst {
constructor(
public name: string, public attrs: AttrAst[], public inputs: BoundElementPropertyAst[],
public outputs: BoundEventAst[], public references: ReferenceAst[],
public directives: DirectiveAst[], public providers: ProviderAst[],
public hasViewContainer: boolean, public queryMatches: QueryMatch[],
public children: TemplateAst[], public ngContentIndex: number|null,
public sourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
}
}
/**
* A `<ng-template>` element included in an Angular template.
*/
export class EmbeddedTemplateAst implements TemplateAst {
constructor(
public attrs: AttrAst[], public outputs: BoundEventAst[], public references: ReferenceAst[],
public variables: VariableAst[], public directives: DirectiveAst[],
public providers: ProviderAst[], public hasViewContainer: boolean,
public queryMatches: QueryMatch[], public children: TemplateAst[],
public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
}
/**
* A directive property with a bound value (e.g. `*ngIf="condition").
*/
export class BoundDirectivePropertyAst implements TemplateAst {
constructor(
public directiveName: string, public templateName: string, public value: ASTWithSource,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirectiveProperty(this, context);
}
}
/**
* A directive declared on an element.
*/
export class DirectiveAst implements TemplateAst {
constructor(
public directive: CompileDirectiveSummary, public inputs: BoundDirectivePropertyAst[],
public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[],
public contentQueryStartId: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirective(this, context);
}
}
/**
* A provider declared on an element
*/
export class ProviderAst implements TemplateAst {
constructor(
public token: CompileTokenMetadata, public multiProvider: boolean, public eager: boolean,
public providers: CompileProviderMetadata[], public providerType: ProviderAstType,
public lifecycleHooks: LifecycleHooks[], public sourceSpan: ParseSourceSpan,
readonly isModule: boolean) {}
visit(visitor: TemplateAstVisitor, context: any): any {
// No visit method in the visitor for now...
return null;
}
}
export enum ProviderAstType {
PublicService,
PrivateService,
Component,
Directive,
Builtin
}
/**
* Position where content is to be projected (instance of `<ng-content>` in a template).
*/
export class NgContentAst implements TemplateAst {
constructor(
public index: number, public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
export interface QueryMatch {
queryId: number;
value: CompileTokenMetadata;
}
/**
* A visitor for {@link TemplateAst} trees that will process each node.
*/
export interface TemplateAstVisitor {
// Returning a truthy value from `visit()` will prevent `templateVisitAll()` from the call to
// the typed method and result returned will become the result included in `visitAll()`s
// result array.
visit?(ast: TemplateAst, context: any): any;
visitNgContent(ast: NgContentAst, context: any): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any;
visitElement(ast: ElementAst, context: any): any;
visitReference(ast: ReferenceAst, context: any): any;
visitVariable(ast: VariableAst, context: any): any;
visitEvent(ast: BoundEventAst, context: any): any;
visitElementProperty(ast: BoundElementPropertyAst, context: any): any;
visitAttr(ast: AttrAst, context: any): any;
visitBoundText(ast: BoundTextAst, context: any): any;
visitText(ast: TextAst, context: any): any;
visitDirective(ast: DirectiveAst, context: any): any;
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any;
}
/**
* A visitor that accepts each node but doesn't do anything. It is intended to be used
* as the base class for a visitor that is only interested in a subset of the node types.
*/
export class NullTemplateVisitor implements TemplateAstVisitor {
visitNgContent(ast: NgContentAst, context: any): void {}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): void {}
visitElement(ast: ElementAst, context: any): void {}
visitReference(ast: ReferenceAst, context: any): void {}
visitVariable(ast: VariableAst, context: any): void {}
visitEvent(ast: BoundEventAst, context: any): void {}
visitElementProperty(ast: BoundElementPropertyAst, context: any): void {}
visitAttr(ast: AttrAst, context: any): void {}
visitBoundText(ast: BoundTextAst, context: any): void {}
visitText(ast: TextAst, context: any): void {}
visitDirective(ast: DirectiveAst, context: any): void {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): void {}
}
/**
* Base class that can be used to build a visitor that visits each node
* in an template ast recursively.
*/
export class RecursiveTemplateAstVisitor extends NullTemplateVisitor implements TemplateAstVisitor {
constructor() { super(); }
// Nodes with children
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.references);
visit(ast.variables);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitElement(ast: ElementAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.inputs);
visit(ast.outputs);
visit(ast.references);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitDirective(ast: DirectiveAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.inputs);
visit(ast.hostProperties);
visit(ast.hostEvents);
});
}
protected visitChildren(
context: any,
cb: (visit: (<V extends TemplateAst>(children: V[]|undefined) => void)) => void) {
let results: any[][] = [];
let t = this;
function visit<T extends TemplateAst>(children: T[] | undefined) {
if (children && children.length) results.push(templateVisitAll(t, children, context));
}
cb(visit);
return Array.prototype.concat.apply([], results);
}
}
/**
* Visit every node in a list of {@link TemplateAst}s with the given {@link TemplateAstVisitor}.
*/
export function templateVisitAll(
visitor: TemplateAstVisitor, asts: TemplateAst[], context: any = null): any[] {
const result: any[] = [];
const visit = visitor.visit ?
(ast: TemplateAst) => visitor.visit !(ast, context) || ast.visit(visitor, context) :
(ast: TemplateAst) => ast.visit(visitor, context);
asts.forEach(ast => {
const astResult = visit(ast);
if (astResult) {
result.push(astResult);
}
});
return result;
}
export type TemplateAstPath = AstPath<TemplateAst>;