Pete Bacon Darwin 5ad2097be8 fix(ivy): teach template type checker about template attributes (#29041)
For the template type checking to work correctly, it needs to know
what attributes are bound to expressions or directives, which may
require expressions in the template to be evaluated in a different
scope.

In inline templates, there are attributes that are now marked as
"Template" attributes. We need to ensure that the template
type checking code looks at these "bound" attributes as well as the
"input" attributes.

PR Close #29041
2019-03-07 11:27:36 -08:00

250 lines
10 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 {SecurityContext} from '../core';
import {AST, BindingType, BoundElementProperty, ParsedEvent, ParsedEventType} from '../expression_parser/ast';
import {AST as I18nAST} from '../i18n/i18n_ast';
import {ParseSourceSpan} from '../parse_util';
export interface Node {
sourceSpan: ParseSourceSpan;
visit<Result>(visitor: Visitor<Result>): Result;
}
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitText(this); }
}
export class BoundText implements Node {
constructor(public value: AST, public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundText(this); }
}
export class TextAttribute implements Node {
constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan, public i18n?: I18nAST) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitTextAttribute(this); }
}
export class BoundAttribute implements Node {
constructor(
public name: string, public type: BindingType, public securityContext: SecurityContext,
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
public i18n?: I18nAST) {}
static fromBoundElementProperty(prop: BoundElementProperty, i18n?: I18nAST) {
return new BoundAttribute(
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan, i18n);
}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundAttribute(this); }
}
export class BoundEvent implements Node {
constructor(
public name: string, public type: ParsedEventType, public handler: AST,
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan,
public handlerSpan: ParseSourceSpan) {}
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 BoundEvent(
event.name, event.type, event.handler, target, phase, event.sourceSpan, event.handlerSpan);
}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
}
export class Element implements Node {
constructor(
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {
// If the element is empty then the source span should include any closing tag
if (children.length === 0 && startSourceSpan && endSourceSpan) {
this.sourceSpan = {...sourceSpan, end: endSourceSpan.end};
}
}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); }
}
export class Template implements Node {
constructor(
public tagName: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public templateAttrs: (BoundAttribute|TextAttribute)[],
public children: Node[], public references: Reference[], public variables: Variable[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitTemplate(this); }
}
export class Content implements Node {
constructor(
public selector: string, public attributes: TextAttribute[],
public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitContent(this); }
}
export class Variable implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitVariable(this); }
}
export class Reference implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitReference(this); }
}
export class Icu implements Node {
constructor(
public vars: {[name: string]: BoundText},
public placeholders: {[name: string]: Text | BoundText}, public sourceSpan: ParseSourceSpan,
public i18n?: I18nAST) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitIcu(this); }
}
export interface Visitor<Result = any> {
// Returning a truthy value from `visit()` will prevent `visitAll()` from the call to the typed
// method and result returned will become the result included in `visitAll()`s result array.
visit?(node: Node): Result;
visitElement(element: Element): Result;
visitTemplate(template: Template): Result;
visitContent(content: Content): Result;
visitVariable(variable: Variable): Result;
visitReference(reference: Reference): Result;
visitTextAttribute(attribute: TextAttribute): Result;
visitBoundAttribute(attribute: BoundAttribute): Result;
visitBoundEvent(attribute: BoundEvent): Result;
visitText(text: Text): Result;
visitBoundText(text: BoundText): Result;
visitIcu(icu: Icu): Result;
}
export class NullVisitor implements Visitor<void> {
visitElement(element: Element): void {}
visitTemplate(template: Template): void {}
visitContent(content: Content): void {}
visitVariable(variable: Variable): void {}
visitReference(reference: Reference): void {}
visitTextAttribute(attribute: TextAttribute): void {}
visitBoundAttribute(attribute: BoundAttribute): void {}
visitBoundEvent(attribute: BoundEvent): void {}
visitText(text: Text): void {}
visitBoundText(text: BoundText): void {}
visitIcu(icu: Icu): void {}
}
export class RecursiveVisitor implements Visitor<void> {
visitElement(element: Element): void {
visitAll(this, element.attributes);
visitAll(this, element.children);
visitAll(this, element.references);
}
visitTemplate(template: Template): void {
visitAll(this, template.attributes);
visitAll(this, template.children);
visitAll(this, template.references);
visitAll(this, template.variables);
}
visitContent(content: Content): void {}
visitVariable(variable: Variable): void {}
visitReference(reference: Reference): void {}
visitTextAttribute(attribute: TextAttribute): void {}
visitBoundAttribute(attribute: BoundAttribute): void {}
visitBoundEvent(attribute: BoundEvent): void {}
visitText(text: Text): void {}
visitBoundText(text: BoundText): void {}
visitIcu(icu: Icu): void {}
}
export class TransformVisitor implements Visitor<Node> {
visitElement(element: Element): Node {
const newAttributes = transformAll(this, element.attributes);
const newInputs = transformAll(this, element.inputs);
const newOutputs = transformAll(this, element.outputs);
const newChildren = transformAll(this, element.children);
const newReferences = transformAll(this, element.references);
if (newAttributes != element.attributes || newInputs != element.inputs ||
newOutputs != element.outputs || newChildren != element.children ||
newReferences != element.references) {
return new Element(
element.name, newAttributes, newInputs, newOutputs, newChildren, newReferences,
element.sourceSpan, element.startSourceSpan, element.endSourceSpan);
}
return element;
}
visitTemplate(template: Template): Node {
const newAttributes = transformAll(this, template.attributes);
const newInputs = transformAll(this, template.inputs);
const newOutputs = transformAll(this, template.outputs);
const newTemplateAttrs = transformAll(this, template.templateAttrs);
const newChildren = transformAll(this, template.children);
const newReferences = transformAll(this, template.references);
const newVariables = transformAll(this, template.variables);
if (newAttributes != template.attributes || newInputs != template.inputs ||
newOutputs != template.outputs || newTemplateAttrs != template.templateAttrs ||
newChildren != template.children || newReferences != template.references ||
newVariables != template.variables) {
return new Template(
template.tagName, newAttributes, newInputs, newOutputs, newTemplateAttrs, newChildren,
newReferences, newVariables, template.sourceSpan, template.startSourceSpan,
template.endSourceSpan);
}
return template;
}
visitContent(content: Content): Node { return content; }
visitVariable(variable: Variable): Node { return variable; }
visitReference(reference: Reference): Node { return reference; }
visitTextAttribute(attribute: TextAttribute): Node { return attribute; }
visitBoundAttribute(attribute: BoundAttribute): Node { return attribute; }
visitBoundEvent(attribute: BoundEvent): Node { return attribute; }
visitText(text: Text): Node { return text; }
visitBoundText(text: BoundText): Node { return text; }
visitIcu(icu: Icu): Node { return icu; }
}
export function visitAll<Result>(visitor: Visitor<Result>, nodes: Node[]): Result[] {
const result: Result[] = [];
if (visitor.visit) {
for (const node of nodes) {
const newNode = visitor.visit(node) || node.visit(visitor);
}
} else {
for (const node of nodes) {
const newNode = node.visit(visitor);
if (newNode) {
result.push(newNode);
}
}
}
return result;
}
export function transformAll<Result extends Node>(
visitor: Visitor<Node>, nodes: Result[]): Result[] {
const result: Result[] = [];
let changed = false;
for (const node of nodes) {
const newNode = node.visit(visitor);
if (newNode) {
result.push(newNode as Result);
}
changed = changed || newNode != node;
}
return changed ? result : nodes;
}