refactor(compiler): Simplify some code in ingest (#53457)

Currently Template Pipeline's ingest phase is very complex, especially when it comes to ingesting bindings.

In this commit, we make some superficial simplifications, in preparation for a larger refactoring. For example, we pull out common code such as `convertAstWithInterpolation` and the `i18n.Message` checks. This enormously shrinks the main binding ingestion functions.

In addition, we reorder the binding kind and flags code above `ingestBindings`, so that `ingestBindings` and `ingestBinding` can be viewed together.

PR Close #53457
This commit is contained in:
Dylan Hunn
2023-12-08 14:23:30 -08:00
committed by Alex Rickabaugh
parent 1faa61f573
commit 876f90ab5d
3 changed files with 179 additions and 172 deletions
@@ -517,7 +517,10 @@ export interface ListenerOp extends Op<CreateOp> {
*/
export function createListenerOp(
target: XrefId, targetSlot: SlotHandle, name: string, tag: string|null,
animationPhase: string|null, hostListener: boolean, sourceSpan: ParseSourceSpan): ListenerOp {
handlerOps: Array<UpdateOp>, animationPhase: string|null, hostListener: boolean,
sourceSpan: ParseSourceSpan): ListenerOp {
const handlerList = new OpList<UpdateOp>();
handlerList.push(handlerOps);
return {
kind: OpKind.Listener,
target,
@@ -525,7 +528,7 @@ export function createListenerOp(
tag,
hostListener,
name,
handlerOps: new OpList(),
handlerOps: handlerList,
handlerFnName: null,
consumesDollarEvent: false,
isAnimationListener: animationPhase !== null,
@@ -19,7 +19,7 @@ import {icuFromI18nMessage, isSingleI18nIcu} from '../../../render3/view/i18n/ut
import {BindingParser} from '../../../template_parser/binding_parser';
import * as ir from '../ir';
import {ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation';
import {CompilationUnit, ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation';
import {BINARY_OPERATORS, namespaceForKey, prefixWithNamespace} from './conversion';
const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder;
@@ -108,7 +108,7 @@ export function ingestHostAttribute(
export function ingestHostEvent(job: HostBindingCompilationJob, event: e.ParsedEvent) {
const eventBinding = ir.createListenerOp(
job.root.xref, new ir.SlotHandle(), event.name, null, event.targetOrPhase, true,
job.root.xref, new ir.SlotHandle(), event.name, null, [], event.targetOrPhase, true,
event.sourceSpan);
// TODO: Can this be a chain?
eventBinding.handlerOps.push(ir.createStatementOp(new o.ReturnStatement(
@@ -240,7 +240,7 @@ function ingestTemplate(unit: ViewCompilationUnit, tmpl: t.Template): void {
}
/**
* Ingest a literal text node from the AST into the given `ViewCompilation`.
* Ingest a content node from the AST into the given `ViewCompilation`.
*/
function ingestContent(unit: ViewCompilationUnit, content: t.Content): void {
if (content.i18n !== undefined && !(content.i18n instanceof i18n.TagPlaceholder)) {
@@ -250,9 +250,14 @@ function ingestContent(unit: ViewCompilationUnit, content: t.Content): void {
const op = ir.createProjectionOp(
unit.job.allocateXrefId(), content.selector, content.i18n, attrs, content.sourceSpan);
for (const attr of content.attributes) {
ingestBinding(
let b = createBinding(
unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, null, attr.i18n);
SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, null, asMessage(attr.i18n));
if (b?.kind === ir.OpKind.Binding) {
unit.update.push(b);
} else if (b?.kind === ir.OpKind.ExtractedAttribute) {
unit.create.push(b);
}
}
unit.create.push(op);
}
@@ -747,134 +752,23 @@ function convertAst(
}
}
/**
* Checks whether the given template is a plain ng-template (as opposed to another kind of template
* such as a structural directive template or control flow template). This is checked based on the
* tagName. We can expect that only plain ng-templates will come through with a tagName of
* 'ng-template'.
*
* Here are some of the cases we expect:
*
* | Angular HTML | Template tagName |
* | ---------------------------------- | ------------------ |
* | `<ng-template>` | 'ng-template' |
* | `<div *ngIf="true">` | 'div' |
* | `<svg><ng-template>` | 'svg:ng-template' |
* | `@if (true) {` | 'Conditional' |
* | `<ng-template *ngIf>` (plain) | 'ng-template' |
* | `<ng-template *ngIf>` (structural) | null |
*/
function isPlainTemplate(tmpl: t.Template) {
return splitNsName(tmpl.tagName ?? '')[1] === 'ng-template';
}
/**
* Process all of the bindings on an element-like structure in the template AST and convert them
* to their IR representation.
*/
function ingestBindings(
unit: ViewCompilationUnit, op: ir.ElementOpBase, element: t.Element|t.Template,
templateKind: ir.TemplateKind|null): void {
let flags: BindingFlags = BindingFlags.None;
let hasI18nAttributes = false;
if (element instanceof t.Template) {
flags |= BindingFlags.OnNgTemplateElement;
if (isPlainTemplate(element)) {
flags |= BindingFlags.BindingTargetsTemplate;
}
const templateAttrFlags =
flags | BindingFlags.BindingTargetsTemplate | BindingFlags.IsStructuralTemplateAttribute;
for (const attr of element.templateAttrs) {
if (attr instanceof t.TextAttribute) {
ingestBinding(
unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, templateAttrFlags | BindingFlags.TextValue,
templateKind, attr.i18n);
hasI18nAttributes ||= attr.i18n !== undefined;
} else {
ingestBinding(
unit, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.securityContext,
attr.sourceSpan, templateAttrFlags, templateKind, attr.i18n);
hasI18nAttributes ||= attr.i18n !== undefined;
}
}
}
for (const attr of element.attributes) {
// This is only attribute TextLiteral bindings, such as `attr.foo="bar"`. This can never be
// `[attr.foo]="bar"` or `attr.foo="{{bar}}"`, both of which will be handled as inputs with
// `BindingType.Attribute`.
ingestBinding(
unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, flags | BindingFlags.TextValue, templateKind,
attr.i18n);
hasI18nAttributes ||= attr.i18n !== undefined;
}
for (const input of element.inputs) {
ingestBinding(
unit, op.xref, input.name, input.value, input.type, input.unit, input.securityContext,
input.sourceSpan, flags, templateKind, input.i18n);
hasI18nAttributes ||= input.i18n !== undefined;
}
for (const output of element.outputs) {
let listenerOp: ir.ListenerOp;
if (output.type === e.ParsedEventType.Animation) {
if (output.phase === null) {
throw Error('Animation listener should have a phase');
}
}
if (element instanceof t.Template && !isPlainTemplate(element)) {
unit.create.push(ir.createExtractedAttributeOp(
op.xref, ir.BindingKind.Property, output.name, null, null, null));
continue;
}
listenerOp = ir.createListenerOp(
op.xref, op.handle, output.name, op.tag, output.phase, false, output.sourceSpan);
// if output.handler is a chain, then push each statement from the chain separately, and
// return the last one?
let handlerExprs: e.AST[];
let handler: e.AST = output.handler;
if (handler instanceof e.ASTWithSource) {
handler = handler.ast;
}
if (handler instanceof e.Chain) {
handlerExprs = handler.expressions;
} else {
handlerExprs = [handler];
}
if (handlerExprs.length === 0) {
throw new Error('Expected listener to have non-empty expression list.');
}
const expressions = handlerExprs.map(expr => convertAst(expr, unit.job, output.handlerSpan));
const returnExpr = expressions.pop()!;
for (const expr of expressions) {
const stmtOp =
ir.createStatementOp<ir.UpdateOp>(new o.ExpressionStatement(expr, expr.sourceSpan));
listenerOp.handlerOps.push(stmtOp);
}
listenerOp.handlerOps.push(
ir.createStatementOp(new o.ReturnStatement(returnExpr, returnExpr.sourceSpan)));
unit.create.push(listenerOp);
}
// TODO: Perhaps we could do this in a phase? (It likely wouldn't change the slot indices.)
if (hasI18nAttributes) {
unit.create.push(
ir.createI18nAttributesOp(unit.job.allocateXrefId(), new ir.SlotHandle(), op.xref));
}
function convertAstWithInterpolation(
job: CompilationJob, value: e.AST|o.Expression, i18nMeta?: i18n.Message): o.Expression|
ir.Interpolation {
let expression: o.Expression|ir.Interpolation;
if (value instanceof e.Interpolation) {
expression = new ir.Interpolation(
value.strings, value.expressions.map(e => convertAst(e, job, null)),
Object.keys(i18nMeta?.placeholders ?? {}));
} else if (value instanceof e.AST) {
expression = convertAst(value, job, null);
} else {
expression = value;
}
return expression;
}
// TODO: Can we populate Template binding kinds in ingest?
const BINDING_KINDS = new Map<e.BindingType, ir.BindingKind>([
[e.BindingType.Property, ir.BindingKind.Property],
[e.BindingType.Attribute, ir.BindingKind.Attribute],
@@ -907,57 +801,163 @@ enum BindingFlags {
OnNgTemplateElement = 0b1000,
}
function ingestBinding(
/**
* Checks whether the given template is a plain ng-template (as opposed to another kind of template
* such as a structural directive template or control flow template). This is checked based on the
* tagName. We can expect that only plain ng-templates will come through with a tagName of
* 'ng-template'.
*
* Here are some of the cases we expect:
*
* | Angular HTML | Template tagName |
* | ---------------------------------- | ------------------ |
* | `<ng-template>` | 'ng-template' |
* | `<div *ngIf="true">` | 'div' |
* | `<svg><ng-template>` | 'svg:ng-template' |
* | `@if (true) {` | 'Conditional' |
* | `<ng-template *ngIf>` (plain) | 'ng-template' |
* | `<ng-template *ngIf>` (structural) | null |
*/
function isPlainTemplate(tmpl: t.Template) {
return splitNsName(tmpl.tagName ?? '')[1] === 'ng-template';
}
/**
* Ensures that the i18nMeta, if provided, is an i18n.Message.
*/
function asMessage(i18nMeta?: i18n.I18nMeta): i18n.Message|undefined {
if (i18nMeta === undefined) {
return undefined;
}
if (!(i18nMeta instanceof i18n.Message)) {
throw Error(`Unhandled i18n metadata type for binding: ${i18nMeta.constructor.name}`);
}
return i18nMeta;
}
/**
* Process all of the bindings on an element-like structure in the template AST and convert them
* to their IR representation.
*/
function ingestBindings(
unit: ViewCompilationUnit, op: ir.ElementOpBase, element: t.Element|t.Template,
templateKind: ir.TemplateKind|null): void {
let flags: BindingFlags = BindingFlags.None;
let bindings = new Array<ir.BindingOp|ir.ExtractedAttributeOp|null>();
if (element instanceof t.Template) {
flags |= BindingFlags.OnNgTemplateElement;
if (isPlainTemplate(element)) {
flags |= BindingFlags.BindingTargetsTemplate;
}
const templateAttrFlags =
flags | BindingFlags.BindingTargetsTemplate | BindingFlags.IsStructuralTemplateAttribute;
for (const attr of element.templateAttrs) {
if (attr instanceof t.TextAttribute) {
bindings.push(createBinding(
unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, templateAttrFlags | BindingFlags.TextValue,
templateKind, asMessage(attr.i18n)));
} else {
bindings.push(createBinding(
unit, op.xref, attr.name, astOf(attr.value), attr.type, attr.unit, attr.securityContext,
attr.sourceSpan, templateAttrFlags, templateKind, asMessage(attr.i18n)));
}
}
}
for (const attr of element.attributes) {
// This is only attribute TextLiteral bindings, such as `attr.foo="bar"`. This can never be
// `[attr.foo]="bar"` or `attr.foo="{{bar}}"`, both of which will be handled as inputs with
// `BindingType.Attribute`.
bindings.push(createBinding(
unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, flags | BindingFlags.TextValue, templateKind,
asMessage(attr.i18n)));
}
for (const input of element.inputs) {
bindings.push(createBinding(
unit, op.xref, input.name, astOf(input.value), input.type, input.unit,
input.securityContext, input.sourceSpan, flags, templateKind, asMessage(input.i18n)));
}
unit.create.push(bindings.filter(
(b): b is ir.ExtractedAttributeOp => b?.kind === ir.OpKind.ExtractedAttribute));
unit.update.push(bindings.filter((b): b is ir.BindingOp => b?.kind === ir.OpKind.Binding));
for (const output of element.outputs) {
if (output.type === e.ParsedEventType.Animation && output.phase === null) {
throw Error('Animation listener should have a phase');
}
if (element instanceof t.Template && !isPlainTemplate(element)) {
unit.create.push(ir.createExtractedAttributeOp(
op.xref, ir.BindingKind.Property, output.name, null, null, null));
continue;
}
unit.create.push(ir.createListenerOp(
op.xref, op.handle, output.name, op.tag,
makeListenerHandlerOps(unit, output.handler, output.handlerSpan), output.phase, false,
output.sourceSpan));
}
// TODO: Perhaps we could do this in a phase? (It likely wouldn't change the slot indices.)
if (bindings.some(b => b?.i18nMessage) !== null) {
unit.create.push(
ir.createI18nAttributesOp(unit.job.allocateXrefId(), new ir.SlotHandle(), op.xref));
}
}
function createBinding(
view: ViewCompilationUnit, xref: ir.XrefId, name: string, value: e.AST|o.Expression,
type: e.BindingType, unit: string|null, securityContext: SecurityContext,
sourceSpan: ParseSourceSpan, flags: BindingFlags, templateKind: ir.TemplateKind|null,
i18nMeta: i18n.I18nMeta|undefined): void {
if (value instanceof e.ASTWithSource) {
value = value.ast;
}
if (i18nMeta !== undefined && !(i18nMeta instanceof i18n.Message)) {
throw Error(`Unhandled i18n metadata type for binding: ${i18nMeta.constructor.name}`);
}
i18nMessage: i18n.Message|undefined): ir.BindingOp|ir.ExtractedAttributeOp|null {
if (flags & BindingFlags.OnNgTemplateElement && !(flags & BindingFlags.BindingTargetsTemplate) &&
(type === e.BindingType.Property || type === e.BindingType.Class ||
type === e.BindingType.Style)) {
// This binding only exists for later const extraction, and is not an actual binding to be
// created.
view.create.push(ir.createExtractedAttributeOp(
xref, ir.BindingKind.Property, name, null, null, i18nMeta ?? null));
return;
}
let expression: o.Expression|ir.Interpolation;
// TODO: We could easily generate source maps for subexpressions in these cases, but
// TemplateDefinitionBuilder does not. Should we do so?
if (value instanceof e.Interpolation) {
let i18nPlaceholders: string[] = [];
if (i18nMeta !== undefined) {
i18nPlaceholders = Object.keys(i18nMeta.placeholders);
}
expression = new ir.Interpolation(
value.strings, value.expressions.map(expr => convertAst(expr, view.job, null)),
i18nPlaceholders);
} else if (value instanceof e.AST) {
expression = convertAst(value, view.job, null);
} else {
expression = value;
return ir.createExtractedAttributeOp(
xref, ir.BindingKind.Property, name, null, null, i18nMessage ?? null);
}
if (type === e.BindingType.Attribute && !(flags & BindingFlags.TextValue) &&
templateKind === ir.TemplateKind.Structural) {
// TODO: big comment about why this is stupid.
return;
return null;
}
const kind: ir.BindingKind = BINDING_KINDS.get(type)!;
view.update.push(ir.createBindingOp(
xref, kind, name, expression, unit, securityContext, !!(flags & BindingFlags.TextValue),
!!(flags & BindingFlags.IsStructuralTemplateAttribute), templateKind, i18nMeta ?? null,
sourceSpan));
return ir.createBindingOp(
xref, BINDING_KINDS.get(type)!, name,
convertAstWithInterpolation(view.job, value, i18nMessage), unit, securityContext,
!!(flags & BindingFlags.TextValue), !!(flags & BindingFlags.IsStructuralTemplateAttribute),
templateKind, i18nMessage ?? null, sourceSpan);
}
function makeListenerHandlerOps(
unit: CompilationUnit, handler: e.AST, handlerSpan: ParseSourceSpan): ir.UpdateOp[] {
handler = astOf(handler);
const handlerOps = new Array<ir.UpdateOp>();
let handlerExprs: e.AST[] = handler instanceof e.Chain ? handler.expressions : [handler];
if (handlerExprs.length === 0) {
throw new Error('Expected listener to have non-empty expression list.');
}
const expressions = handlerExprs.map(expr => convertAst(expr, unit.job, handlerSpan));
const returnExpr = expressions.pop()!;
handlerOps.push(...expressions.map(
e => ir.createStatementOp<ir.UpdateOp>(new o.ExpressionStatement(e, e.sourceSpan))));
handlerOps.push(ir.createStatementOp(new o.ReturnStatement(returnExpr, returnExpr.sourceSpan)));
return handlerOps;
}
function astOf(ast: e.AST|e.ASTWithSource): e.AST {
return ast instanceof e.ASTWithSource ? ast.ast : ast;
}
/**
@@ -1055,10 +1055,15 @@ function ingestControlFlowInsertionPoint(
// and they can be used in directive matching (in the case of `Template.templateAttrs`).
if (root !== null) {
for (const attr of root.attributes) {
ingestBinding(
let b = createBinding(
unit, xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null,
SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, ir.TemplateKind.Block,
attr.i18n);
asMessage(attr.i18n));
if (b?.kind === ir.OpKind.Binding) {
unit.update.push(b);
} else if (b?.kind === ir.OpKind.ExtractedAttribute) {
unit.create.push(b);
}
}
const tagName = root instanceof t.Element ? root.name : root.tagName;
@@ -31,7 +31,6 @@ export function extractAttributes(job: CompilationJob): void {
// kind in the consts array.
bindingKind = ir.BindingKind.I18n;
} else if (op.isStructuralTemplateAttribute) {
// TODO: How do i18n attributes on templates work?!
bindingKind = ir.BindingKind.Template;
} else {
bindingKind = ir.BindingKind.Property;