JoostK 489cef6ea2 feat(ivy): include value spans for attributes, variables and references (#30181)
Template AST nodes for (bound) attributes, variables and references will
now retain a reference to the source span of their value, which allows
for more accurate type check diagnostics.

PR Close #30181
2019-07-25 16:36:32 -07:00

255 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 valueSpan?: 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,
prop.valueSpan, 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 = new ParseSourceSpan(sourceSpan.start, 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,
public valueSpan?: 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,
public valueSpan?: 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;
}