Alex Rickabaugh b0eca85e51 refactor(compiler): compile{Component,Directive} take only local information (#23545)
Previously, the compileComponent() and compileDirective() APIs still required
the output of global analysis, even though they only read local information
from that output.

With this refactor, compileComponent() and compileDirective() now define
their inputs explicitly, with the new interfaces R3ComponentMetadata and
R3DirectiveMetadata. compileComponentGlobal() and compileDirectiveGlobal()
are introduced and convert from global analysis output into the new metadata
format.

This refactor also splits out the view compiler into separate files as
r3_view_compiler_local.ts was getting unwieldy.

Finally, this refactor also splits out generation of DI factory functions
into a separate r3_factory utility as the logic is utilized between different
compilers.

PR Close #23545
2018-05-08 13:57:20 -07:00

444 lines
18 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 {StaticSymbol} from '../../aot/static_symbol';
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, sanitizeIdentifier, tokenReference} from '../../compile_metadata';
import {CompileReflector} from '../../compile_reflector';
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../../constant_pool';
import * as core from '../../core';
import {AST, AstMemoryEfficientTransformer, BindingPipe, BoundElementBindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
import {Identifiers} from '../../identifiers';
import {LifecycleHooks} from '../../lifecycle_reflector';
import * as o from '../../output/output_ast';
import {ParseSourceSpan, typeSourceSpan} from '../../parse_util';
import {CssSelector, SelectorMatcher} from '../../selector';
import {BindingParser} from '../../template_parser/binding_parser';
import {OutputContext, error} from '../../util';
import * as t from './../r3_ast';
import {R3DependencyMetadata, R3ResolvedDependencyType, compileFactoryFunction, dependenciesFromGlobalMetadata} from './../r3_factory';
import {Identifiers as R3} from './../r3_identifiers';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {BindingScope, TemplateDefinitionBuilder} from './template';
import {CONTEXT_NAME, DefinitionMap, ID_SEPARATOR, MEANING_SEPARATOR, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator, unsupported} from './util';
function baseDirectiveFields(
meta: R3DirectiveMetadata, constantPool: ConstantPool,
bindingParser: BindingParser): DefinitionMap {
const definitionMap = new DefinitionMap();
// e.g. `type: MyDirective`
definitionMap.set('type', meta.type);
// e.g. `selectors: [['', 'someDir', '']]`
definitionMap.set('selectors', createDirectiveSelector(meta.selector !));
const queryDefinitions = createQueryDefinitions(meta.queries, constantPool);
// e.g. `factory: () => new MyApp(injectElementRef())`
definitionMap.set('factory', compileFactoryFunction({
name: meta.name,
fnOrClass: meta.type,
deps: meta.deps,
useNew: true,
injectFn: R3.directiveInject,
useOptionalParam: false,
extraResults: queryDefinitions,
}));
// e.g. `hostBindings: (dirIndex, elIndex) => { ... }
definitionMap.set('hostBindings', createHostBindingsFunction(meta, bindingParser));
// e.g. `attributes: ['role', 'listbox']`
definitionMap.set('attributes', createHostAttributesArray(meta));
// e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs));
// e.g 'outputs: {a: 'a'}`
definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs));
return definitionMap;
}
/**
* Compile a directive for the render3 runtime as defined by the `R3DirectiveMetadata`.
*/
export function compileDirective(
meta: R3DirectiveMetadata, constantPool: ConstantPool,
bindingParser: BindingParser): R3DirectiveDef {
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]);
const type =
new o.ExpressionType(o.importExpr(R3.DirectiveDef, [new o.ExpressionType(meta.type)]));
return {expression, type};
}
/**
* Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`.
*/
export function compileComponent(
meta: R3ComponentMetadata, constantPool: ConstantPool,
bindingParser: BindingParser): R3ComponentDef {
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
const selector = meta.selector && CssSelector.parse(meta.selector);
const firstSelector = selector && selector[0];
// e.g. `attr: ["class", ".my.app"]`
// This is optional an only included if the first selector of a component specifies attributes.
if (firstSelector) {
const selectorAttributes = firstSelector.getAttrs();
if (selectorAttributes.length) {
definitionMap.set(
'attrs', constantPool.getConstLiteral(
o.literalArr(selectorAttributes.map(
value => value != null ? o.literal(value) : o.literal(undefined))),
/* forceShared */ true));
}
}
// Generate the CSS matcher that recognize directive
let directiveMatcher: SelectorMatcher|null = null;
if (meta.directives.size) {
const matcher = new SelectorMatcher();
meta.directives.forEach((expression, selector: string) => {
matcher.addSelectables(CssSelector.parse(selector), expression);
});
directiveMatcher = matcher;
}
// e.g. `template: function MyComponent_Template(_ctx, _cm) {...}`
const templateTypeName = meta.name;
const templateName = templateTypeName ? `${templateTypeName}_Template` : null;
const directivesUsed = new Set<o.Expression>();
const pipesUsed = new Set<o.Expression>();
const template = meta.template;
const templateFunctionExpression =
new TemplateDefinitionBuilder(
constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName,
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed)
.buildTemplateFunction(
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
definitionMap.set('template', templateFunctionExpression);
// e.g. `directives: [MyDirective]`
if (directivesUsed.size) {
definitionMap.set('directives', o.literalArr(Array.from(directivesUsed)));
}
// e.g. `pipes: [MyPipe]`
if (pipesUsed.size) {
definitionMap.set('pipes', o.literalArr(Array.from(pipesUsed)));
}
// e.g. `features: [NgOnChangesFeature(MyComponent)]`
const features: o.Expression[] = [];
if (meta.lifecycle.usesOnChanges) {
features.push(o.importExpr(R3.NgOnChangesFeature, null, null).callFn([meta.type]));
}
if (features.length) {
definitionMap.set('features', o.literalArr(features));
}
const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]);
const type =
new o.ExpressionType(o.importExpr(R3.ComponentDef, [new o.ExpressionType(meta.type)]));
return {expression, type};
}
/**
* A wrapper around `compileDirective` which depends on render2 global analysis data as its input
* instead of the `R3DirectiveMetadata`.
*
* `R3DirectiveMetadata` is computed from `CompileDirectiveMetadata` and other statically reflected
* information.
*/
export function compileDirectiveFromRender2(
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
bindingParser: BindingParser) {
const name = identifierName(directive.type) !;
name || error(`Cannot resolver the name of ${directive.type}`);
const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Directive);
const meta = directiveMetadataFromGlobalMetadata(directive, outputCtx, reflector);
const res = compileDirective(meta, outputCtx.constantPool, bindingParser);
// Create the partial class to be merged with the actual class.
outputCtx.statements.push(new o.ClassStmt(
name, null,
[new o.ClassField(definitionField, o.INFERRED_TYPE, [o.StmtModifier.Static], res.expression)],
[], new o.ClassMethod(null, [], []), []));
}
/**
* A wrapper around `compileComponent` which depends on render2 global analysis data as its input
* instead of the `R3DirectiveMetadata`.
*
* `R3ComponentMetadata` is computed from `CompileDirectiveMetadata` and other statically reflected
* information.
*/
export function compileComponentFromRender2(
outputCtx: OutputContext, component: CompileDirectiveMetadata, nodes: t.Node[],
hasNgContent: boolean, ngContentSelectors: string[], reflector: CompileReflector,
bindingParser: BindingParser, directiveTypeBySel: Map<string, any>,
pipeTypeByName: Map<string, any>) {
const name = identifierName(component.type) !;
name || error(`Cannot resolver the name of ${component.type}`);
const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component);
const summary = component.toSummary();
// Compute the R3ComponentMetadata from the CompileDirectiveMetadata
const meta: R3ComponentMetadata = {
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
selector: component.selector,
template: {
nodes, hasNgContent, ngContentSelectors,
},
lifecycle: {
usesOnChanges:
component.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges),
},
directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx),
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
};
const res = compileComponent(meta, outputCtx.constantPool, bindingParser);
// Create the partial class to be merged with the actual class.
outputCtx.statements.push(new o.ClassStmt(
name, null,
[new o.ClassField(definitionField, o.INFERRED_TYPE, [o.StmtModifier.Static], res.expression)],
[], new o.ClassMethod(null, [], []), []));
}
/**
* Compute `R3DirectiveMetadata` given `CompileDirectiveMetadata` and a `CompileReflector`.
*/
function directiveMetadataFromGlobalMetadata(
directive: CompileDirectiveMetadata, outputCtx: OutputContext,
reflector: CompileReflector): R3DirectiveMetadata {
const summary = directive.toSummary();
const name = identifierName(directive.type) !;
name || error(`Cannot resolver the name of ${directive.type}`);
return {
name,
type: outputCtx.importExpr(directive.type.reference),
typeSourceSpan:
typeSourceSpan(directive.isComponent ? 'Component' : 'Directive', directive.type),
selector: directive.selector,
deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector),
queries: queriesFromGlobalMetadata(directive.queries, outputCtx),
host: {
attributes: directive.hostAttributes,
listeners: summary.hostListeners,
properties: summary.hostProperties,
},
inputs: directive.inputs,
outputs: directive.outputs,
};
}
/**
* Convert `CompileQueryMetadata` into `R3QueryMetadata`.
*/
function queriesFromGlobalMetadata(
queries: CompileQueryMetadata[], outputCtx: OutputContext): R3QueryMetadata[] {
return queries.map(query => {
let read: o.Expression|null = null;
if (query.read && query.read.identifier) {
read = outputCtx.importExpr(query.read.identifier.reference);
}
return {
propertyName: query.propertyName,
first: query.first,
predicate: selectorsFromGlobalMetadata(query.selectors, outputCtx),
descendants: query.descendants, read,
};
});
}
/**
* Convert `CompileTokenMetadata` for query selectors into either an expression for a predicate
* type, or a list of string predicates.
*/
function selectorsFromGlobalMetadata(
selectors: CompileTokenMetadata[], outputCtx: OutputContext): o.Expression|string[] {
if (selectors.length > 1 || (selectors.length == 1 && selectors[0].value)) {
const selectorStrings = selectors.map(value => value.value as string);
selectorStrings.some(value => !value) &&
error('Found a type among the string selectors expected');
return outputCtx.constantPool.getConstLiteral(
o.literalArr(selectorStrings.map(value => o.literal(value))));
}
if (selectors.length == 1) {
const first = selectors[0];
if (first.identifier) {
return outputCtx.importExpr(first.identifier.reference);
}
}
error('Unexpected query form');
return o.NULL_EXPR;
}
/**
*
* @param meta
* @param constantPool
*/
function createQueryDefinitions(
queries: R3QueryMetadata[], constantPool: ConstantPool): o.Expression[]|undefined {
const queryDefinitions: o.Expression[] = [];
for (let i = 0; i < queries.length; i++) {
const query = queries[i];
const predicate = getQueryPredicate(query, constantPool);
// e.g. r3.Q(null, somePredicate, false) or r3.Q(null, ['div'], false)
const parameters = [
o.literal(null, o.INFERRED_TYPE),
predicate,
o.literal(query.descendants),
];
if (query.read) {
parameters.push(query.read);
}
queryDefinitions.push(o.importExpr(R3.query).callFn(parameters));
}
return queryDefinitions.length > 0 ? queryDefinitions : undefined;
}
// Turn a directive selector into an R3-compatible selector for directive def
function createDirectiveSelector(selector: string): o.Expression {
return asLiteral(core.parseSelectorToR3Selector(selector));
}
function createHostAttributesArray(meta: R3DirectiveMetadata): o.Expression|null {
const values: o.Expression[] = [];
const attributes = meta.host.attributes;
for (let key of Object.getOwnPropertyNames(attributes)) {
const value = attributes[key];
values.push(o.literal(key), o.literal(value));
}
if (values.length > 0) {
return o.literalArr(values);
}
return null;
}
// Return a host binding function or null if one is not necessary.
function createHostBindingsFunction(
meta: R3DirectiveMetadata, bindingParser: BindingParser): o.Expression|null {
const statements: o.Statement[] = [];
const temporary = temporaryAllocator(statements, TEMPORARY_NAME);
const hostBindingSourceSpan = meta.typeSourceSpan;
// Calculate the queries
for (let index = 0; index < meta.queries.length; index++) {
const query = meta.queries[index];
// e.g. r3.qR(tmp = r3.ld(dirIndex)[1]) && (r3.ld(dirIndex)[0].someDir = tmp);
const getDirectiveMemory = o.importExpr(R3.load).callFn([o.variable('dirIndex')]);
// The query list is at the query index + 1 because the directive itself is in slot 0.
const getQueryList = getDirectiveMemory.key(o.literal(index + 1));
const assignToTemporary = temporary().set(getQueryList);
const callQueryRefresh = o.importExpr(R3.queryRefresh).callFn([assignToTemporary]);
const updateDirective = getDirectiveMemory.key(o.literal(0, o.INFERRED_TYPE))
.prop(query.propertyName)
.set(query.first ? temporary().prop('first') : temporary());
const andExpression = callQueryRefresh.and(updateDirective);
statements.push(andExpression.toStmt());
}
const directiveSummary = metadataAsSummary(meta);
// Calculate the host property bindings
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
const bindingContext = o.importExpr(R3.load).callFn([o.variable('dirIndex')]);
if (bindings) {
for (const binding of bindings) {
const bindingExpr = convertPropertyBinding(
null, bindingContext, binding.expression, 'b', BindingForm.TrySimple,
() => error('Unexpected interpolation'));
statements.push(...bindingExpr.stmts);
statements.push(o.importExpr(R3.elementProperty)
.callFn([
o.variable('elIndex'),
o.literal(binding.name),
o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]),
])
.toStmt());
}
}
// Calculate host event bindings
const eventBindings =
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
if (eventBindings) {
for (const binding of eventBindings) {
const bindingExpr = convertActionBinding(
null, bindingContext, binding.handler, 'b', () => error('Unexpected interpolation'));
const bindingName = binding.name && sanitizeIdentifier(binding.name);
const typeName = meta.name;
const functionName =
typeName && bindingName ? `${typeName}_${bindingName}_HostBindingHandler` : null;
const handler = o.fn(
[new o.FnParam('$event', o.DYNAMIC_TYPE)],
[...bindingExpr.stmts, new o.ReturnStatement(bindingExpr.allowDefault)], o.INFERRED_TYPE,
null, functionName);
statements.push(
o.importExpr(R3.listener).callFn([o.literal(binding.name), handler]).toStmt());
}
}
if (statements.length > 0) {
const typeName = meta.name;
return o.fn(
[
new o.FnParam('dirIndex', o.NUMBER_TYPE),
new o.FnParam('elIndex', o.NUMBER_TYPE),
],
statements, o.INFERRED_TYPE, null, typeName ? `${typeName}_HostBindings` : null);
}
return null;
}
function metadataAsSummary(meta: R3DirectiveMetadata): CompileDirectiveSummary {
// clang-format off
return {
hostAttributes: meta.host.attributes,
hostListeners: meta.host.listeners,
hostProperties: meta.host.properties,
} as CompileDirectiveSummary;
// clang-format on
}
function typeMapToExpressionMap(
map: Map<string, StaticSymbol>, outputCtx: OutputContext): Map<string, o.Expression> {
// Convert each map entry into another entry where the value is an expression importing the type.
const entries = Array.from(map).map(
([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]);
return new Map(entries);
}