feat(ivy): introduce a new compiler API for operating on templates (#26203)
This commit introduces the "t2" API, which processes parsed template ASTs and performs a number of functions such as binding (the process of semantically interpreting cross-references within the template) and directive matching. The API is modeled on TypeScript's TypeChecker API, with oracle methods that give access to collected metadata. This work is a prerequisite for the upcoming template type-checking functionality, and will also become the basis for a refactored TemplateDefinitionBuilder. PR Close #26203
This commit is contained in:

committed by
Jason Aden

parent
a2da485d90
commit
9ed4e3df60
139
packages/compiler/src/render3/view/t2_api.ts
Normal file
139
packages/compiler/src/render3/view/t2_api.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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 {AST} from '../../expression_parser/ast';
|
||||
|
||||
import {BoundAttribute, BoundEvent, Element, Node, Reference, Template, TextAttribute, Variable} from '../r3_ast';
|
||||
|
||||
/*
|
||||
* t2 is the replacement for the `TemplateDefinitionBuilder`. It handles the operations of
|
||||
* analyzing Angular templates, extracting semantic info, and ultimately producing a template
|
||||
* definition function which renders the template using Ivy instructions.
|
||||
*
|
||||
* t2 data is also utilized by the template type-checking facilities to understand a template enough
|
||||
* to generate type-checking code for it.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A logical target for analysis, which could contain a template or other types of bindings.
|
||||
*/
|
||||
export interface Target { template?: Node[]; }
|
||||
|
||||
/**
|
||||
* Metadata regarding a directive that's needed to match it against template elements. This is
|
||||
* provided by a consumer of the t2 APIs.
|
||||
*/
|
||||
export interface DirectiveMeta {
|
||||
/**
|
||||
* Name of the directive class (used for debugging).
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Whether the directive is a component.
|
||||
*/
|
||||
isComponent: boolean;
|
||||
|
||||
/**
|
||||
* Set of inputs which this directive claims.
|
||||
*
|
||||
* Goes from property names to field names.
|
||||
*/
|
||||
inputs: {[property: string]: string};
|
||||
|
||||
/**
|
||||
* Set of outputs which this directive claims.
|
||||
*
|
||||
* Goes from property names to field names.
|
||||
*/
|
||||
outputs: {[property: string]: string};
|
||||
|
||||
/**
|
||||
* Name under which the directive is exported, if any (exportAs in Angular).
|
||||
*
|
||||
* Null otherwise
|
||||
*/
|
||||
exportAs: string|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to the binding API, which processes a template and returns an object similar to the
|
||||
* `ts.TypeChecker`.
|
||||
*
|
||||
* The returned `BoundTarget` has an API for extracting information about the processed target.
|
||||
*/
|
||||
export interface TargetBinder<D extends DirectiveMeta> { bind(target: Target): BoundTarget<D>; }
|
||||
|
||||
/**
|
||||
* Result of performing the binding operation against a `Target`.
|
||||
*
|
||||
* The original `Target` is accessible, as well as a suite of methods for extracting binding
|
||||
* information regarding the `Target`.
|
||||
*
|
||||
* @param DirectiveT directive metadata type
|
||||
*/
|
||||
export interface BoundTarget<DirectiveT extends DirectiveMeta> {
|
||||
/**
|
||||
* Get the original `Target` that was bound.
|
||||
*/
|
||||
readonly target: Target;
|
||||
|
||||
/**
|
||||
* For a given template node (either an `Element` or a `Template`), get the set of directives
|
||||
* which matched the node, if any.
|
||||
*/
|
||||
getDirectivesOfNode(node: Element|Template): DirectiveT[]|null;
|
||||
|
||||
/**
|
||||
* For a given `Reference`, get the reference's target - either an `Element`, a `Template`, or
|
||||
* a directive on a particular node.
|
||||
*/
|
||||
getReferenceTarget(ref: Reference): {directive: DirectiveT, node: Element|Template}|Element
|
||||
|Template|null;
|
||||
|
||||
/**
|
||||
* For a given binding, get the entity to which the binding is being made.
|
||||
*
|
||||
* This will either be a directive or the node itself.
|
||||
*/
|
||||
getConsumerOfBinding(binding: BoundAttribute|BoundEvent|TextAttribute): DirectiveT|Element
|
||||
|Template|null;
|
||||
|
||||
/**
|
||||
* If the given `AST` expression refers to a `Reference` or `Variable` within the `Target`, then
|
||||
* return that.
|
||||
*
|
||||
* Otherwise, returns `null`.
|
||||
*
|
||||
* This is only defined for `AST` expressions that read or write to a property of an
|
||||
* `ImplicitReceiver`.
|
||||
*/
|
||||
getExpressionTarget(expr: AST): Reference|Variable|null;
|
||||
|
||||
/**
|
||||
* Given a particular `Reference` or `Variable`, get the `Template` which created it.
|
||||
*
|
||||
* All `Variable`s are defined on templates, so this will always return a value for a `Variable`
|
||||
* from the `Target`. For `Reference`s this only returns a value if the `Reference` points to a
|
||||
* `Template`. Returns `null` otherwise.
|
||||
*/
|
||||
getTemplateOfSymbol(symbol: Reference|Variable): Template|null;
|
||||
|
||||
/**
|
||||
* Get the nesting level of a particular `Template`.
|
||||
*
|
||||
* This starts at 1 for top-level `Template`s within the `Target` and increases for `Template`s
|
||||
* nested at deeper levels.
|
||||
*/
|
||||
getNestingLevel(template: Template): number;
|
||||
|
||||
/**
|
||||
* Get a list of all the directives used by the target.
|
||||
*/
|
||||
getUsedDirectives(): DirectiveT[];
|
||||
}
|
528
packages/compiler/src/render3/view/t2_binder.ts
Normal file
528
packages/compiler/src/render3/view/t2_binder.ts
Normal file
@ -0,0 +1,528 @@
|
||||
/**
|
||||
* @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 {AST, ImplicitReceiver, MethodCall, PropertyRead, PropertyWrite, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../expression_parser/ast';
|
||||
import {CssSelector, SelectorMatcher} from '../../selector';
|
||||
import {BoundAttribute, BoundEvent, BoundText, Content, Element, Node, Reference, Template, Text, TextAttribute, Variable, Visitor} from '../r3_ast';
|
||||
|
||||
import {BoundTarget, DirectiveMeta, Target, TargetBinder} from './t2_api';
|
||||
import {getAttrsForDirectiveMatching} from './util';
|
||||
|
||||
/**
|
||||
* Processes `Target`s with a given set of directives and performs a binding operation, which
|
||||
* returns an object similar to TypeScript's `ts.TypeChecker` that contains knowledge about the
|
||||
* target.
|
||||
*/
|
||||
export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetBinder<DirectiveT> {
|
||||
constructor(private directiveMatcher: SelectorMatcher<DirectiveT>) {}
|
||||
|
||||
/**
|
||||
* Perform a binding operation on the given `Target` and return a `BoundTarget` which contains
|
||||
* metadata about the types referenced in the template.
|
||||
*/
|
||||
bind(target: Target): BoundTarget<DirectiveT> {
|
||||
if (!target.template) {
|
||||
// TODO(alxhub): handle targets which contain things like HostBindings, etc.
|
||||
throw new Error('Binding without a template not yet supported');
|
||||
}
|
||||
|
||||
// First, parse the template into a `Scope` structure. This operation captures the syntactic
|
||||
// scopes in the template and makes them available for later use.
|
||||
const scope = Scope.apply(target.template);
|
||||
|
||||
// Next, perform directive matching on the template using the `DirectiveBinder`. This returns:
|
||||
// - directives: Map of nodes (elements & ng-templates) to the directives on them.
|
||||
// - bindings: Map of inputs, outputs, and attributes to the directive/element that claims
|
||||
// them. TODO(alxhub): handle multiple directives claiming an input/output/etc.
|
||||
// - references: Map of #references to their targets.
|
||||
const {directives, bindings, references} =
|
||||
DirectiveBinder.apply(target.template, this.directiveMatcher);
|
||||
// Finally, run the TemplateBinder to bind references, variables, and other entities within the
|
||||
// template. This extracts all the metadata that doesn't depend on directive matching.
|
||||
const {expressions, symbols, nestingLevel} = TemplateBinder.apply(target.template, scope);
|
||||
return new R3BoundTarget(
|
||||
target, directives, bindings, references, expressions, symbols, nestingLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a binding scope within a template.
|
||||
*
|
||||
* Any variables, references, or other named entities declared within the template will
|
||||
* be captured and available by name in `namedEntities`. Additionally, child templates will
|
||||
* be analyzed and have their child `Scope`s available in `childScopes`.
|
||||
*/
|
||||
class Scope implements Visitor {
|
||||
/**
|
||||
* Named members of the `Scope`, such as `Reference`s or `Variable`s.
|
||||
*/
|
||||
readonly namedEntities = new Map<string, Reference|Variable>();
|
||||
|
||||
/**
|
||||
* Child `Scope`s for immediately nested `Template`s.
|
||||
*/
|
||||
readonly childScopes = new Map<Template, Scope>();
|
||||
|
||||
private constructor(readonly parentScope?: Scope) {}
|
||||
|
||||
/**
|
||||
* Process a template (either as a `Template` sub-template with variables, or a plain array of
|
||||
* template `Node`s) and construct its `Scope`.
|
||||
*/
|
||||
static apply(template: Template|Node[]): Scope {
|
||||
const scope = new Scope();
|
||||
scope.ingest(template);
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to process the template and populate the `Scope`.
|
||||
*/
|
||||
private ingest(template: Template|Node[]): void {
|
||||
if (template instanceof Template) {
|
||||
// Variables on an <ng-template> are defined in the inner scope.
|
||||
template.variables.forEach(node => this.visitVariable(node));
|
||||
|
||||
// Process the nodes of the template.
|
||||
template.children.forEach(node => node.visit(this));
|
||||
} else {
|
||||
// No overarching `Template` instance, so process the nodes directly.
|
||||
template.forEach(node => node.visit(this));
|
||||
}
|
||||
}
|
||||
|
||||
visitElement(element: Element) {
|
||||
// `Element`s in the template may have `Reference`s which are captured in the scope.
|
||||
element.references.forEach(node => this.visitReference(node));
|
||||
|
||||
// Recurse into the `Element`'s children.
|
||||
element.children.forEach(node => node.visit(this));
|
||||
}
|
||||
|
||||
visitTemplate(template: Template) {
|
||||
// References on a <ng-template> are defined in the outer scope, so capture them before
|
||||
// processing the template's child scope.
|
||||
template.references.forEach(node => this.visitReference(node));
|
||||
|
||||
// Next, create an inner scope and process the template within it.
|
||||
const scope = new Scope(this);
|
||||
scope.ingest(template);
|
||||
this.childScopes.set(template, scope);
|
||||
}
|
||||
|
||||
visitVariable(variable: Variable) {
|
||||
// Declare the variable if it's not already.
|
||||
this.maybeDeclare(variable);
|
||||
}
|
||||
|
||||
visitReference(reference: Reference) {
|
||||
// Declare the variable if it's not already.
|
||||
this.maybeDeclare(reference);
|
||||
}
|
||||
|
||||
// Unused visitors.
|
||||
visitContent(content: Content) {}
|
||||
visitBoundAttribute(attr: BoundAttribute) {}
|
||||
visitBoundEvent(event: BoundEvent) {}
|
||||
visitBoundText(text: BoundText) {}
|
||||
visitText(text: Text) {}
|
||||
visitTextAttribute(attr: TextAttribute) {}
|
||||
|
||||
private maybeDeclare(thing: Reference|Variable) {
|
||||
// Declare something with a name, as long as that name isn't taken.
|
||||
if (!this.namedEntities.has(thing.name)) {
|
||||
this.namedEntities.set(thing.name, thing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a variable within this `Scope`.
|
||||
*
|
||||
* This can recurse into a parent `Scope` if it's available.
|
||||
*/
|
||||
lookup(name: string): Reference|Variable|null {
|
||||
if (this.namedEntities.has(name)) {
|
||||
// Found in the local scope.
|
||||
return this.namedEntities.get(name) !;
|
||||
} else if (this.parentScope !== undefined) {
|
||||
// Not in the local scope, but there's a parent scope so check there.
|
||||
return this.parentScope.lookup(name);
|
||||
} else {
|
||||
// At the top level and it wasn't found.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child scope for a `Template`.
|
||||
*
|
||||
* This should always be defined.
|
||||
*/
|
||||
getChildScope(template: Template): Scope {
|
||||
const res = this.childScopes.get(template);
|
||||
if (res === undefined) {
|
||||
throw new Error(`Assertion error: child scope for ${template} not found`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a template and matches directives on nodes (elements and templates).
|
||||
*
|
||||
* Usually used via the static `apply()` method.
|
||||
*/
|
||||
class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
||||
constructor(
|
||||
private matcher: SelectorMatcher<DirectiveT>,
|
||||
private directives: Map<Element|Template, DirectiveT[]>,
|
||||
private bindings: Map<BoundAttribute|BoundEvent|TextAttribute, DirectiveT|Element|Template>,
|
||||
private references:
|
||||
Map<Reference, {directive: DirectiveT, node: Element|Template}|Element|Template>) {}
|
||||
|
||||
/**
|
||||
* Process a template (list of `Node`s) and perform directive matching against each node.
|
||||
*
|
||||
* @param template the list of template `Node`s to match (recursively).
|
||||
* @param selectorMatcher a `SelectorMatcher` containing the directives that are in scope for
|
||||
* this template.
|
||||
* @returns three maps which contain information about directives in the template: the
|
||||
* `directives` map which lists directives matched on each node, the `bindings` map which
|
||||
* indicates which directives claimed which bindings (inputs, outputs, etc), and the `references`
|
||||
* map which resolves #references (`Reference`s) within the template to the named directive or
|
||||
* template node.
|
||||
*/
|
||||
static apply<DirectiveT extends DirectiveMeta>(
|
||||
template: Node[], selectorMatcher: SelectorMatcher<DirectiveT>): {
|
||||
directives: Map<Element|Template, DirectiveT[]>,
|
||||
bindings: Map<BoundAttribute|BoundEvent|TextAttribute, DirectiveT|Element|Template>,
|
||||
references: Map<Reference, {directive: DirectiveT, node: Element|Template}|Element|Template>,
|
||||
} {
|
||||
const directives = new Map<Element|Template, DirectiveT[]>();
|
||||
const bindings =
|
||||
new Map<BoundAttribute|BoundEvent|TextAttribute, DirectiveT|Element|Template>();
|
||||
const references =
|
||||
new Map<Reference, {directive: DirectiveT, node: Element | Template}|Element|Template>();
|
||||
const matcher = new DirectiveBinder(selectorMatcher, directives, bindings, references);
|
||||
matcher.ingest(template);
|
||||
return {directives, bindings, references};
|
||||
}
|
||||
|
||||
private ingest(template: Node[]): void { template.forEach(node => node.visit(this)); }
|
||||
|
||||
visitElement(element: Element): void { this.visitElementOrTemplate(element.name, element); }
|
||||
|
||||
visitTemplate(template: Template): void { this.visitElementOrTemplate('ng-template', template); }
|
||||
|
||||
visitElementOrTemplate(tag: string, node: Element|Template): void {
|
||||
// First, determine the HTML shape of the node for the purpose of directive matching.
|
||||
// Do this by building up a `CssSelector` for the node.
|
||||
const cssSelector = new CssSelector();
|
||||
cssSelector.setElement(tag);
|
||||
|
||||
// Add attributes to the CSS selector.
|
||||
const attrs = getAttrsForDirectiveMatching(node);
|
||||
Object.getOwnPropertyNames(attrs).forEach((name) => {
|
||||
const value = attrs[name];
|
||||
|
||||
cssSelector.addAttribute(name, value);
|
||||
|
||||
// Treat the 'class' attribute specially.
|
||||
if (name.toLowerCase() === 'class') {
|
||||
const classes = value.trim().split(/\s+/g);
|
||||
classes.forEach(className => cssSelector.addClassName(className));
|
||||
}
|
||||
});
|
||||
|
||||
// Next, use the `SelectorMatcher` to get the list of directives on the node.
|
||||
const directives: DirectiveT[] = [];
|
||||
this.matcher.match(cssSelector, (_, directive) => directives.push(directive));
|
||||
if (directives.length > 0) {
|
||||
this.directives.set(node, directives);
|
||||
}
|
||||
|
||||
// Resolve any references that are created on this node.
|
||||
node.references.forEach(ref => {
|
||||
let dirTarget: DirectiveT|null = null;
|
||||
|
||||
// If the reference expression is empty, then it matches the "primary" directive on the node
|
||||
// (if there is one). Otherwise it matches the host node itself (either an element or
|
||||
// <ng-template> node).
|
||||
if (ref.value.trim() === '') {
|
||||
// This could be a reference to a component if there is one.
|
||||
dirTarget = directives.find(dir => dir.isComponent) || null;
|
||||
} else {
|
||||
// This is a reference to a directive exported via exportAs. One should exist.
|
||||
dirTarget = directives.find(dir => dir.exportAs === ref.value) || null;
|
||||
|
||||
// Check if a matching directive was found, and error if it wasn't.
|
||||
if (dirTarget === null) {
|
||||
// TODO(alxhub): Return an error value here that can be used for template validation.
|
||||
throw new Error(`Assertion error: failed to find directive with exportAs: ${ref.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirTarget !== null) {
|
||||
// This reference points to a directive.
|
||||
this.references.set(ref, {directive: dirTarget, node});
|
||||
} else {
|
||||
// This reference points to the node itself.
|
||||
this.references.set(ref, node);
|
||||
}
|
||||
});
|
||||
|
||||
// Associate bindings on the node with directives or with the node itself.
|
||||
|
||||
// Inputs:
|
||||
[...node.attributes, ...node.inputs].forEach(binding => {
|
||||
let dir = directives.find(dir => dir.inputs.hasOwnProperty(binding.name));
|
||||
if (dir !== undefined) {
|
||||
this.bindings.set(binding, dir);
|
||||
} else {
|
||||
this.bindings.set(binding, node);
|
||||
}
|
||||
});
|
||||
|
||||
// Outputs:
|
||||
node.outputs.forEach(binding => {
|
||||
let dir = directives.find(dir => dir.outputs.hasOwnProperty(binding.name));
|
||||
if (dir !== undefined) {
|
||||
this.bindings.set(binding, dir);
|
||||
} else {
|
||||
this.bindings.set(binding, node);
|
||||
}
|
||||
});
|
||||
|
||||
// Recurse into the node's children.
|
||||
node.children.forEach(child => child.visit(this));
|
||||
}
|
||||
|
||||
// Unused visitors.
|
||||
visitContent(content: Content): void {}
|
||||
visitVariable(variable: Variable): void {}
|
||||
visitReference(reference: Reference): void {}
|
||||
visitTextAttribute(attribute: TextAttribute): void {}
|
||||
visitBoundAttribute(attribute: BoundAttribute): void {}
|
||||
visitBoundEvent(attribute: BoundEvent): void {}
|
||||
visitBoundAttributeOrEvent(node: BoundAttribute|BoundEvent) {}
|
||||
visitText(text: Text): void {}
|
||||
visitBoundText(text: BoundText): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a template and extract metadata about expressions and symbols within.
|
||||
*
|
||||
* This is a companion to the `DirectiveBinder` that doesn't require knowledge of directives matched
|
||||
* within the template in order to operate.
|
||||
*
|
||||
* Expressions are visited by the superclass `RecursiveAstVisitor`, with custom logic provided
|
||||
* by overridden methods from that visitor.
|
||||
*/
|
||||
class TemplateBinder extends RecursiveAstVisitor implements Visitor {
|
||||
private visitNode: (node: Node) => void;
|
||||
|
||||
private constructor(
|
||||
private bindings: Map<AST, Reference|Variable>,
|
||||
private symbols: Map<Reference|Variable, Template>,
|
||||
private nestingLevel: Map<Template, number>, private scope: Scope,
|
||||
private template: Template|null, private level: number) {
|
||||
super();
|
||||
|
||||
// Save a bit of processing time by constructing this closure in advance.
|
||||
this.visitNode = (node: Node) => node.visit(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a template and extract metadata about expressions and symbols within.
|
||||
*
|
||||
* @param template the nodes of the template to process
|
||||
* @param scope the `Scope` of the template being processed.
|
||||
* @returns three maps which contain metadata about the template: `expressions` which interprets
|
||||
* special `AST` nodes in expressions as pointing to references or variables declared within the
|
||||
* template, `symbols` which maps those variables and references to the nested `Template` which
|
||||
* declares them, if any, and `nestingLevel` which associates each `Template` with a integer
|
||||
* nesting level (how many levels deep within the template structure the `Template` is), starting
|
||||
* at 1.
|
||||
*/
|
||||
static apply(template: Node[], scope: Scope): {
|
||||
expressions: Map<AST, Reference|Variable>,
|
||||
symbols: Map<Variable|Reference, Template>,
|
||||
nestingLevel: Map<Template, number>,
|
||||
} {
|
||||
const expressions = new Map<AST, Reference|Variable>();
|
||||
const symbols = new Map<Variable|Reference, Template>();
|
||||
const nestingLevel = new Map<Template, number>();
|
||||
// The top-level template has nesting level 0.
|
||||
const binder = new TemplateBinder(
|
||||
expressions, symbols, nestingLevel, scope, template instanceof Template ? template : null,
|
||||
0);
|
||||
binder.ingest(template);
|
||||
return {expressions, symbols, nestingLevel};
|
||||
}
|
||||
|
||||
private ingest(template: Template|Node[]): void {
|
||||
if (template instanceof Template) {
|
||||
// For <ng-template>s, process inputs, outputs, variables, and child nodes. References were
|
||||
// processed in the scope of the containing template.
|
||||
template.inputs.forEach(this.visitNode);
|
||||
template.outputs.forEach(this.visitNode);
|
||||
template.variables.forEach(this.visitNode);
|
||||
template.children.forEach(this.visitNode);
|
||||
|
||||
// Set the nesting level.
|
||||
this.nestingLevel.set(template, this.level);
|
||||
} else {
|
||||
// Visit each node from the top-level template.
|
||||
template.forEach(this.visitNode);
|
||||
}
|
||||
}
|
||||
|
||||
visitElement(element: Element) {
|
||||
// Vist the inputs, outputs, and children of the element.
|
||||
element.inputs.forEach(this.visitNode);
|
||||
element.outputs.forEach(this.visitNode);
|
||||
element.children.forEach(this.visitNode);
|
||||
}
|
||||
|
||||
visitTemplate(template: Template) {
|
||||
// First, visit the inputs, outputs of the template node.
|
||||
template.inputs.forEach(this.visitNode);
|
||||
template.outputs.forEach(this.visitNode);
|
||||
|
||||
// References are also evaluated in the outer context.
|
||||
template.references.forEach(this.visitNode);
|
||||
|
||||
// Next, recurse into the template using its scope, and bumping the nesting level up by one.
|
||||
const childScope = this.scope.getChildScope(template);
|
||||
const binder = new TemplateBinder(
|
||||
this.bindings, this.symbols, this.nestingLevel, childScope, template, this.level + 1);
|
||||
binder.ingest(template);
|
||||
}
|
||||
|
||||
visitVariable(variable: Variable) {
|
||||
// Register the `Variable` as a symbol in the current `Template`.
|
||||
if (this.template !== null) {
|
||||
this.symbols.set(variable, this.template);
|
||||
}
|
||||
}
|
||||
|
||||
visitReference(reference: Reference) {
|
||||
// Register the `Reference` as a symbol in the current `Template`.
|
||||
if (this.template !== null) {
|
||||
this.symbols.set(reference, this.template);
|
||||
}
|
||||
}
|
||||
|
||||
// Unused template visitors
|
||||
|
||||
visitText(text: Text) {}
|
||||
visitContent(content: Content) {}
|
||||
visitTextAttribute(attribute: TextAttribute) {}
|
||||
|
||||
// The remaining visitors are concerned with processing AST expressions within template bindings
|
||||
|
||||
visitBoundAttribute(attribute: BoundAttribute) { attribute.value.visit(this); }
|
||||
|
||||
visitBoundEvent(event: BoundEvent) { event.handler.visit(this); }
|
||||
|
||||
visitBoundText(text: BoundText) { text.value.visit(this); }
|
||||
|
||||
|
||||
|
||||
// These five types of AST expressions can refer to expression roots, which could be variables
|
||||
// or references in the current scope.
|
||||
|
||||
visitPropertyRead(ast: PropertyRead, context: any): any {
|
||||
this.maybeMap(context, ast, ast.name);
|
||||
return super.visitPropertyRead(ast, context);
|
||||
}
|
||||
|
||||
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
|
||||
this.maybeMap(context, ast, ast.name);
|
||||
return super.visitSafePropertyRead(ast, context);
|
||||
}
|
||||
|
||||
visitPropertyWrite(ast: PropertyWrite, context: any): any {
|
||||
this.maybeMap(context, ast, ast.name);
|
||||
return super.visitPropertyWrite(ast, context);
|
||||
}
|
||||
|
||||
visitMethodCall(ast: MethodCall, context: any): any {
|
||||
this.maybeMap(context, ast, ast.name);
|
||||
return super.visitMethodCall(ast, context);
|
||||
}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
|
||||
this.maybeMap(context, ast, ast.name);
|
||||
return super.visitSafeMethodCall(ast, context);
|
||||
}
|
||||
|
||||
private maybeMap(
|
||||
scope: Scope, ast: PropertyRead|SafePropertyRead|PropertyWrite|MethodCall|SafeMethodCall,
|
||||
name: string): void {
|
||||
// If the receiver of the expression isn't the `ImplicitReceiver`, this isn't the root of an
|
||||
// `AST` expression that maps to a `Variable` or `Reference`.
|
||||
if (!(ast.receiver instanceof ImplicitReceiver)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether the name exists in the current scope. If so, map it. Otherwise, the name is
|
||||
// probably a property on the top-level component context.
|
||||
let target = this.scope.lookup(name);
|
||||
if (target !== null) {
|
||||
this.bindings.set(ast, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata container for a `Target` that allows queries for specific bits of metadata.
|
||||
*
|
||||
* See `BoundTarget` for documentation on the individual methods.
|
||||
*/
|
||||
export class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<DirectiveT> {
|
||||
constructor(
|
||||
readonly target: Target, private directives: Map<Element|Template, DirectiveT[]>,
|
||||
private bindings: Map<BoundAttribute|BoundEvent|TextAttribute, DirectiveT|Element|Template>,
|
||||
private references:
|
||||
Map<BoundAttribute|BoundEvent|Reference|TextAttribute,
|
||||
{directive: DirectiveT, node: Element|Template}|Element|Template>,
|
||||
private exprTargets: Map<AST, Reference|Variable>,
|
||||
private symbols: Map<Reference|Variable, Template>,
|
||||
private nestingLevel: Map<Template, number>) {}
|
||||
|
||||
getDirectivesOfNode(node: Element|Template): DirectiveT[]|null {
|
||||
return this.directives.get(node) || null;
|
||||
}
|
||||
|
||||
getReferenceTarget(ref: Reference): {directive: DirectiveT, node: Element|Template}|Element
|
||||
|Template|null {
|
||||
return this.references.get(ref) || null;
|
||||
}
|
||||
|
||||
getConsumerOfBinding(binding: BoundAttribute|BoundEvent|TextAttribute): DirectiveT|Element
|
||||
|Template|null {
|
||||
return this.bindings.get(binding) || null;
|
||||
}
|
||||
|
||||
getExpressionTarget(expr: AST): Reference|Variable|null {
|
||||
return this.exprTargets.get(expr) || null;
|
||||
}
|
||||
|
||||
getTemplateOfSymbol(symbol: Reference|Variable): Template|null {
|
||||
return this.symbols.get(symbol) || null;
|
||||
}
|
||||
|
||||
getNestingLevel(template: Template): number { return this.nestingLevel.get(template) || 0; }
|
||||
|
||||
getUsedDirectives(): DirectiveT[] {
|
||||
const set = new Set<DirectiveT>();
|
||||
this.directives.forEach(dirs => dirs.forEach(dir => set.add(dir)));
|
||||
return Array.from(set.values());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user