feat(ivy): ngtsc compiles @Component, @Directive, @NgModule (#24427)

This change supports compilation of components, directives, and modules
within ngtsc. Support is not complete, but is enough to compile and test
//packages/core/test/bundling/todo in full AOT mode. Code size benefits
are not yet achieved as //packages/core itself does not get compiled, and
some decorators (e.g. @Input) are not stripped, leading to unwanted code
being retained by the tree-shaker. This will be improved in future commits.

PR Close #24427
This commit is contained in:
Alex Rickabaugh
2018-05-31 15:50:02 -07:00
committed by Miško Hevery
parent 0f7e4fae20
commit 27bc7dcb43
69 changed files with 1884 additions and 607 deletions

View File

@ -25,6 +25,7 @@ ts_library(
tsconfig = ":tsconfig",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/transform",
],
)

View File

@ -0,0 +1,17 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "annotations",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/annotations",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform",
],
)

View File

@ -0,0 +1,13 @@
/**
* @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
*/
export {ComponentDecoratorHandler} from './src/component';
export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable';
export {NgModuleDecoratorHandler} from './src/ng_module';
export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope';

View File

@ -0,0 +1,117 @@
/**
* @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 {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, reflectNonStaticField, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {extractDirectiveMetadata} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
const EMPTY_MAP = new Map<string, Expression>();
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Component' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveMetadata = extractDirectiveMetadata(node, decorator, this.checker);
if (directiveMetadata === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies
// that no analysis was produced.
return {};
}
// Next, read the `@Component`-specific fields.
const component = reflectObjectLiteral(meta);
// Resolve and parse the template.
if (!component.has('template')) {
throw new Error(`For now, components must directly have a template.`);
}
const templateExpr = component.get('template') !;
const templateStr = staticallyResolve(templateExpr, this.checker);
if (typeof templateStr !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
}
let preserveWhitespaces: boolean = false;
if (component.has('preserveWhitespaces')) {
const value = staticallyResolve(component.get('preserveWhitespaces') !, this.checker);
if (typeof value !== 'boolean') {
throw new Error(`preserveWhitespaces must resolve to a boolean if present`);
}
preserveWhitespaces = value;
}
const template = parseTemplate(
templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`,
{preserveWhitespaces});
if (template.errors !== undefined) {
throw new Error(
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
}
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (directiveMetadata.selector !== null) {
this.scopeRegistry.registerSelector(node, directiveMetadata.selector);
}
return {
analysis: {
...directiveMetadata,
template,
viewQueries: [],
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP,
directives: EMPTY_MAP,
}
};
}
compile(node: ts.ClassDeclaration, analysis: R3ComponentMetadata): CompileResult {
const pool = new ConstantPool();
// Check whether this component was registered with an NgModule. If so, it should be compiled
// under that module's compilation scope.
const scope = this.scopeRegistry.lookupCompilationScope(node);
if (scope !== null) {
// Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during compile() the whole compilation unit has been
// fully analyzed.
analysis = {...analysis, ...scope};
}
const res = compileComponentFromMetadata(analysis, pool, makeBindingParser());
return {
field: 'ngComponentDef',
initializer: res.expression,
statements: pool.statements,
type: res.type,
};
}
}

View File

@ -0,0 +1,212 @@
/**
* @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 {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, staticallyResolve} from '../../metadata';
import {DecoratedNode, getDecoratedClassElements, reflectNonStaticField, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Directive' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
const analysis = extractDirectiveMetadata(node, decorator, this.checker);
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) {
this.scopeRegistry.registerSelector(node, analysis.selector);
}
return {analysis};
}
compile(node: ts.ClassDeclaration, analysis: R3DirectiveMetadata): CompileResult {
const pool = new ConstantPool();
const res = compileDirectiveFromMetadata(analysis, pool, makeBindingParser());
return {
field: 'ngDirectiveDef',
initializer: res.expression,
statements: pool.statements,
type: res.type,
};
}
}
/**
* Helper function to extract metadata from a `Directive` or `Component`.
*/
export function extractDirectiveMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker): R3DirectiveMetadata|
undefined {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
const directive = reflectObjectLiteral(meta);
if (directive.has('jit')) {
// The only allowed value is true, so there's no need to expand further.
return undefined;
}
// Precompute a list of ts.ClassElements that have decorators. This includes things like @Input,
// @Output, @HostBinding, etc.
const decoratedElements = getDecoratedClassElements(clazz, checker);
// Construct the map of inputs both from the @Directive/@Component decorator, and the decorated
// fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker);
const inputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Input'), checker);
// And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
const outputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Output'), checker);
// Parse the selector.
let selector = '';
if (directive.has('selector')) {
const resolved = staticallyResolve(directive.get('selector') !, checker);
if (typeof resolved !== 'string') {
throw new Error(`Selector must be a string`);
}
selector = resolved;
}
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = reflectNonStaticField(clazz, 'ngOnChanges') !== null;
return {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, checker),
host: {
attributes: {},
listeners: {},
properties: {},
},
lifecycle: {
usesOnChanges,
},
inputs: {...inputsFromMeta, ...inputsFromFields},
outputs: {...outputsFromMeta, ...outputsFromFields},
queries: [], selector,
type: new WrappedNodeExpr(clazz.name !),
typeSourceSpan: null !,
};
}
function assertIsStringArray(value: any[]): value is string[] {
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string') {
throw new Error(`Failed to resolve @Directive.inputs[${i}] to a string`);
}
}
return true;
}
type DecoratedProperty = DecoratedNode<ts.PropertyDeclaration|ts.AccessorDeclaration>;
/**
* Find all fields in the array of `DecoratedNode`s that have a decorator of the given type.
*/
function findDecoratedFields(
elements: DecoratedNode<ts.ClassElement>[], decoratorModule: string,
decoratorName: string): DecoratedProperty[] {
return elements
.map(entry => {
const element = entry.element;
// Only consider properties and accessors. Filter out everything else.
if (!ts.isPropertyDeclaration(element) && !ts.isAccessor(element)) {
return null;
}
// Extract the array of matching decorators (there could be more than one).
const decorators = entry.decorators.filter(
decorator => decorator.name === decoratorName && decorator.from === decoratorModule);
if (decorators.length === 0) {
// No matching decorators, don't include this element.
return null;
}
return {element, decorators};
})
// Filter out nulls.
.filter(entry => entry !== null) as DecoratedProperty[];
}
/**
* Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the
* correctly shaped metadata object.
*/
function parseFieldToPropertyMapping(
directive: Map<string, ts.Expression>, field: string,
checker: ts.TypeChecker): {[field: string]: string} {
if (!directive.has(field)) {
return EMPTY_OBJECT;
}
// Resolve the field of interest from the directive metadata to a string[].
const metaValues = staticallyResolve(directive.get(field) !, checker);
if (!Array.isArray(metaValues) || !assertIsStringArray(metaValues)) {
throw new Error(`Failed to resolve @Directive.${field}`);
}
return metaValues.reduce(
(results, value) => {
// Either the value is 'field' or 'field: property'. In the first case, `property` will
// be undefined, in which case the field name should also be used as the property name.
const [field, property] = value.split(':', 2).map(str => str.trim());
results[field] = property || field;
return results;
},
{} as{[field: string]: string});
}
/**
* Parse property decorators (e.g. `Input` or `Output`) and return the correctly shaped metadata
* object.
*/
function parseDecoratedFields(
fields: DecoratedProperty[], checker: ts.TypeChecker): {[field: string]: string} {
return fields.reduce(
(results, field) => {
const fieldName = (field.element.name as ts.Identifier).text;
field.decorators.forEach(decorator => {
// The decorator either doesn't have an argument (@Input()) in which case the property
// name is used, or it has one argument (@Output('named')).
if (decorator.args.length === 0) {
results[fieldName] = fieldName;
} else if (decorator.args.length === 1) {
const property = staticallyResolve(decorator.args[0], checker);
if (typeof property !== 'string') {
throw new Error(`Decorator argument must resolve to a string`);
}
results[fieldName] = property;
} else {
// Too many arguments.
throw new Error(
`Decorator must have 0 or 1 arguments, got ${decorator.args.length} argument(s)`);
}
});
return results;
},
{} as{[field: string]: string});
}

View File

@ -11,14 +11,15 @@ import * as ts from 'typescript';
import {Decorator} from '../../metadata';
import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform/src/api';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
import {getConstructorDependencies} from './util';
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableCompilerAdapter implements CompilerAdapter<R3InjectableMetadata> {
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
constructor(private checker: ts.TypeChecker) {}
detect(decorator: Decorator[]): Decorator|undefined {
@ -31,11 +32,12 @@ export class InjectableCompilerAdapter implements CompilerAdapter<R3InjectableMe
};
}
compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): AddStaticFieldInstruction {
compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): CompileResult {
const res = compileIvyInjectable(analysis);
return {
field: 'ngInjectableDef',
initializer: res.expression,
statements: [],
type: res.type,
};
}
@ -105,38 +107,7 @@ function extractInjectableMetadata(
}
}
function getConstructorDependencies(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
ctorParams.forEach(param => {
let tokenExpr = param.typeValueExpr;
let optional = false, self = false, skipSelf = false;
param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => {
if (dec.name === 'Inject') {
if (dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
} else if (dec.name === 'Optional') {
optional = true;
} else if (dec.name === 'SkipSelf') {
skipSelf = true;
} else if (dec.name === 'Self') {
self = true;
} else {
throw new Error(`Unexpected decorator ${dec.name} on parameter.`);
}
if (tokenExpr === null) {
throw new Error(`No suitable token for parameter!`);
}
});
const token = new WrappedNodeExpr(tokenExpr);
useType.push(
{token, optional, self, skipSelf, host: false, resolved: R3ResolvedDependencyType.Token});
});
return useType;
}
function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata {
const meta: R3DependencyMetadata = {

View File

@ -0,0 +1,116 @@
/**
* @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 {ConstantPool, Expression, R3DirectiveMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {referenceToExpression} from './util';
/**
* Compiles @NgModule annotations to ngModuleDef fields.
*
* TODO(alxhub): handle injector side of things as well.
*/
export class NgModuleDecoratorHandler implements DecoratorHandler<R3NgModuleMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'NgModule' && decorator.from === '@angular/core');
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3NgModuleMetadata> {
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
const ngModule = reflectObjectLiteral(meta);
if (ngModule.has('jit')) {
// The only allowed value is true, so there's no need to expand further.
return {};
}
// Extract the module declarations, imports, and exports.
let declarations: Reference[] = [];
if (ngModule.has('declarations')) {
const declarationMeta = staticallyResolve(ngModule.get('declarations') !, this.checker);
declarations = resolveTypeList(declarationMeta, 'declarations');
}
let imports: Reference[] = [];
if (ngModule.has('imports')) {
const importsMeta = staticallyResolve(ngModule.get('imports') !, this.checker);
imports = resolveTypeList(importsMeta, 'imports');
}
let exports: Reference[] = [];
if (ngModule.has('exports')) {
const exportsMeta = staticallyResolve(ngModule.get('exports') !, this.checker);
exports = resolveTypeList(exportsMeta, 'exports');
}
// Register this module's information with the SelectorScopeRegistry. This ensures that during
// the compile() phase, the module's metadata is available for selector scope computation.
this.scopeRegistry.registerModule(node, {declarations, imports, exports});
const context = node.getSourceFile();
return {
analysis: {
type: new WrappedNodeExpr(node.name !),
bootstrap: [],
declarations: declarations.map(decl => referenceToExpression(decl, context)),
exports: exports.map(exp => referenceToExpression(exp, context)),
imports: imports.map(imp => referenceToExpression(imp, context)),
emitInline: false,
},
};
}
compile(node: ts.ClassDeclaration, analysis: R3NgModuleMetadata): CompileResult {
const res = compileNgModule(analysis);
return {
field: 'ngModuleDef',
initializer: res.expression,
statements: [],
type: res.type,
};
}
}
/**
* Compute a list of `Reference`s from a resolved metadata value.
*/
function resolveTypeList(resolvedList: ResolvedValue, name: string): Reference[] {
const refList: Reference[] = [];
if (!Array.isArray(resolvedList)) {
throw new Error(`Expected array when reading property ${name}`);
}
resolvedList.forEach((entry, idx) => {
if (Array.isArray(entry)) {
// Recurse into nested arrays.
refList.push(...resolveTypeList(entry, name));
} else if (entry instanceof Reference) {
if (!entry.expressable) {
throw new Error(`Value at position ${idx} in ${name} array is not expressable`);
} else if (!ts.isClassDeclaration(entry.node)) {
throw new Error(`Value at position ${idx} in ${name} array is not a class declaration`);
}
refList.push(entry);
} else {
// TODO(alxhub): expand ModuleWithProviders.
throw new Error(`Value at position ${idx} in ${name} array is not a reference`);
}
});
return refList;
}

View File

@ -0,0 +1,353 @@
/**
* @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 {Expression, ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteReference, Reference, reflectStaticField, reflectTypeEntityToDeclaration} from '../../metadata';
import {referenceToExpression} from './util';
/**
* Metadata extracted for a given NgModule that can be used to compute selector scopes.
*/
export interface ModuleData {
declarations: Reference[];
imports: Reference[];
exports: Reference[];
}
/**
* Transitively expanded maps of directives and pipes visible to a component being compiled in the
* context of some module.
*/
export interface CompilationScope<T> {
directives: Map<string, T>;
pipes: Map<string, T>;
}
/**
* Both transitively expanded scopes for a given NgModule.
*/
interface SelectorScopes {
/**
* Set of components, directives, and pipes visible to all components being compiled in the
* context of some module.
*/
compilation: Reference[];
/**
* Set of components, directives, and pipes added to the compilation scope of any module importing
* some module.
*/
exported: Reference[];
}
/**
* Registry which records and correlates static analysis information of Angular types.
*
* Once a compilation unit's information is fed into the SelectorScopeRegistry, it can be asked to
* produce transitive `CompilationScope`s for components.
*/
export class SelectorScopeRegistry {
/**
* Map of modules declared in the current compilation unit to their (local) metadata.
*/
private _moduleToData = new Map<ts.ClassDeclaration, ModuleData>();
/**
* Map of modules to their cached `CompilationScope`s.
*/
private _compilationScopeCache = new Map<ts.ClassDeclaration, CompilationScope<Reference>>();
/**
* Map of components/directives to their selector.
*/
private _directiveToSelector = new Map<ts.ClassDeclaration, string>();
/**
* Map of pipes to their name.
*/
private _pipeToName = new Map<ts.ClassDeclaration, string>();
/**
* Map of components/directives/pipes to their module.
*/
private _declararedTypeToModule = new Map<ts.ClassDeclaration, ts.ClassDeclaration>();
constructor(private checker: ts.TypeChecker) {}
/**
* Register a module's metadata with the registry.
*/
registerModule(node: ts.ClassDeclaration, data: ModuleData): void {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
if (this._moduleToData.has(node)) {
throw new Error(`Module already registered: ${node.name!.text}`);
}
this._moduleToData.set(node, data);
// Register all of the module's declarations in the context map as belonging to this module.
data.declarations.forEach(decl => {
this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.ClassDeclaration, node);
});
}
/**
* Register the selector of a component or directive with the registry.
*/
registerSelector(node: ts.ClassDeclaration, selector: string): void {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
if (this._directiveToSelector.has(node)) {
throw new Error(`Selector already registered: ${node.name!.text} ${selector}`);
}
this._directiveToSelector.set(node, selector);
}
/**
* Register the name of a pipe with the registry.
*/
registerPipe(node: ts.ClassDeclaration, name: string): void { this._pipeToName.set(node, name); }
/**
* Produce the compilation scope of a component, which is determined by the module that declares
* it.
*/
lookupCompilationScope(node: ts.ClassDeclaration): CompilationScope<Expression>|null {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
// If the component has no associated module, then it has no compilation scope.
if (!this._declararedTypeToModule.has(node)) {
return null;
}
const module = this._declararedTypeToModule.get(node) !;
// Compilation scope computation is somewhat expensive, so it's cached. Check the cache for
// the module.
if (this._compilationScopeCache.has(module)) {
// The compilation scope was cached.
const scope = this._compilationScopeCache.get(module) !;
// The scope as cached is in terms of References, not Expressions. Converting between them
// requires knowledge of the context file (in this case, the component node's source file).
return convertScopeToExpressions(scope, node.getSourceFile());
}
// This is the first time the scope for this module is being computed.
const directives = new Map<string, Reference>();
const pipes = new Map<string, Reference>();
// Process the declaration scope of the module, and lookup the selector of every declared type.
// The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
// was not imported from a .d.ts source.
this.lookupScopes(module !, /* ngModuleImportedFrom */ null).compilation.forEach(ref => {
const selector =
this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.ClassDeclaration);
// Only directives/components with selectors get added to the scope.
if (selector != null) {
directives.set(selector, ref);
}
});
const scope: CompilationScope<Reference> = {directives, pipes};
// Many components may be compiled in the same scope, so cache it.
this._compilationScopeCache.set(node, scope);
// Convert References to Expressions in the context of the component's source file.
return convertScopeToExpressions(scope, node.getSourceFile());
}
/**
* Lookup `SelectorScopes` for a given module.
*
* This function assumes that if the given module was imported from an absolute path
* (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well
* as imports and exports from other modules that are relatively imported.
*/
private lookupScopes(node: ts.ClassDeclaration, ngModuleImportedFrom: string|null):
SelectorScopes {
let data: ModuleData|null = null;
// Either this module was analyzed directly, or has a precompiled ngModuleDef.
if (this._moduleToData.has(node)) {
// The module was analyzed before, and thus its data is available.
data = this._moduleToData.get(node) !;
} else {
// The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type
// annotation that specifies the needed metadata.
if (ngModuleImportedFrom === null) {
// TODO(alxhub): handle hand-compiled ngModuleDef in the current Program.
throw new Error(`Need to read .d.ts module but ngModuleImportedFrom is unspecified`);
}
data = this._readMetadataFromCompiledClass(node, ngModuleImportedFrom);
// Note that data here could still be null, if the class didn't have a precompiled
// ngModuleDef.
}
if (data === null) {
throw new Error(`Module not registered: ${node.name!.text}`);
}
return {
compilation: [
...data.declarations,
// Expand imports to the exported scope of those imports.
...flatten(data.imports.map(
ref => this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported)),
// And include the compilation scope of exported modules.
...flatten(
data.exports.filter(ref => this._moduleToData.has(ref.node as ts.ClassDeclaration))
.map(
ref =>
this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported))
],
exported: flatten(data.exports.map(ref => {
if (this._moduleToData.has(ref.node as ts.ClassDeclaration)) {
return this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported;
} else {
return [ref];
}
})),
};
}
/**
* Lookup the selector of a component or directive class.
*
* Potentially this class is declared in a .d.ts file or otherwise has a manually created
* ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read
* to determine the selector.
*/
private lookupDirectiveSelector(node: ts.ClassDeclaration): string|null {
if (this._directiveToSelector.has(node)) {
return this._directiveToSelector.get(node) !;
} else {
return this._readSelectorFromCompiledClass(node);
}
}
private lookupPipeName(node: ts.ClassDeclaration): string|undefined {
return this._pipeToName.get(node);
}
/**
* Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts
* file, or in a .ts file with a handwritten definition).
*
* @param clazz the class of interest
* @param ngModuleImportedFrom module specifier of the import path to assume for all declarations
* stemming from this module.
*/
private _readMetadataFromCompiledClass(clazz: ts.ClassDeclaration, ngModuleImportedFrom: string):
ModuleData|null {
// This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`.
// TODO(alxhub): investigate caching of .d.ts module metadata.
const ngModuleDef = reflectStaticField(clazz, 'ngModuleDef');
if (ngModuleDef === null) {
return null;
} else if (
// Validate that the shape of the ngModuleDef type is correct.
ngModuleDef.type === undefined || !ts.isTypeReferenceNode(ngModuleDef.type) ||
ngModuleDef.type.typeArguments === undefined ||
ngModuleDef.type.typeArguments.length !== 4) {
return null;
}
// Read the ModuleData out of the type arguments.
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
return {
declarations: this._extractReferencesFromType(declarationMetadata, ngModuleImportedFrom),
exports: this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom),
imports: this._extractReferencesFromType(importMetadata, ngModuleImportedFrom),
};
}
/**
* Get the selector from type metadata for a class with a precompiled ngComponentDef or
* ngDirectiveDef.
*/
private _readSelectorFromCompiledClass(clazz: ts.ClassDeclaration): string|null {
const def =
reflectStaticField(clazz, 'ngComponentDef') || reflectStaticField(clazz, 'ngDirectiveDef');
if (def === null) {
// No definition could be found.
return null;
} else if (
def.type === undefined || !ts.isTypeReferenceNode(def.type) ||
def.type.typeArguments === undefined || def.type.typeArguments.length !== 2) {
// The type metadata was the wrong shape.
return null;
}
const type = def.type.typeArguments[1];
if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) {
// The type metadata was the wrong type.
return null;
}
return type.literal.text;
}
/**
* Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to
* them.
*
* This operation assumes that these types should be imported from `ngModuleImportedFrom` unless
* they themselves were imported from another absolute path.
*/
private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string): Reference[] {
if (!ts.isTupleTypeNode(def)) {
return [];
}
return def.elementTypes.map(element => {
if (!ts.isTypeReferenceNode(element)) {
throw new Error(`Expected TypeReferenceNode`);
}
const type = element.typeName;
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
const clazz = node as ts.ClassDeclaration;
return new AbsoluteReference(node, clazz.name !, moduleName, clazz.name !.text);
});
}
}
function flatten<T>(array: T[][]): T[] {
return array.reduce((accum, subArray) => {
accum.push(...subArray);
return accum;
}, [] as T[]);
}
function absoluteModuleName(ref: Reference): string|null {
const name = (ref.node as ts.ClassDeclaration).name !.text;
if (!(ref instanceof AbsoluteReference)) {
return null;
}
return ref.moduleName;
}
function convertReferenceMap(
map: Map<string, Reference>, context: ts.SourceFile): Map<string, Expression> {
return new Map<string, Expression>(Array.from(map.entries()).map(([selector, ref]): [
string, Expression
] => [selector, referenceToExpression(ref, context)]));
}
function convertScopeToExpressions(
scope: CompilationScope<Reference>, context: ts.SourceFile): CompilationScope<Expression> {
const directives = convertReferenceMap(scope.directives, context);
const pipes = convertReferenceMap(scope.pipes, context);
return {directives, pipes};
}

View File

@ -0,0 +1,84 @@
/**
* @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 {Expression, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference, reflectConstructorParameters} from '../../metadata';
import {reflectImportedIdentifier} from '../../metadata/src/reflector';
export function getConstructorDependencies(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
ctorParams.forEach(param => {
let tokenExpr = param.typeValueExpr;
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => {
if (dec.name === 'Inject') {
if (dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
} else if (dec.name === 'Optional') {
optional = true;
} else if (dec.name === 'SkipSelf') {
skipSelf = true;
} else if (dec.name === 'Self') {
self = true;
} else if (dec.name === 'Host') {
host = true;
} else if (dec.name === 'Attribute') {
if (dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Attribute().`);
}
tokenExpr = dec.args[0];
resolved = R3ResolvedDependencyType.Attribute;
} else {
throw new Error(`Unexpected decorator ${dec.name} on parameter.`);
}
});
if (tokenExpr === null) {
throw new Error(
`No suitable token for parameter ${(param.name as ts.Identifier).text} of class ${clazz.name!.text} with decorators ${param.decorators.map(dec => dec.from + '#' + dec.name).join(',')}`);
}
if (ts.isIdentifier(tokenExpr)) {
const importedSymbol = reflectImportedIdentifier(tokenExpr, checker);
if (importedSymbol !== null && importedSymbol.from === '@angular/core') {
switch (importedSymbol.name) {
case 'ElementRef':
resolved = R3ResolvedDependencyType.ElementRef;
break;
case 'Injector':
resolved = R3ResolvedDependencyType.Injector;
break;
case 'TemplateRef':
resolved = R3ResolvedDependencyType.TemplateRef;
break;
case 'ViewContainerRef':
resolved = R3ResolvedDependencyType.ViewContainerRef;
break;
default:
// Leave as a Token or Attribute.
}
}
}
const token = new WrappedNodeExpr(tokenExpr);
useType.push({token, optional, self, skipSelf, host, resolved});
});
return useType;
}
export function referenceToExpression(ref: Reference, context: ts.SourceFile): Expression {
const exp = ref.toExpression(context);
if (exp === null) {
throw new Error(`Could not refer to ${ts.SyntaxKind[ref.node.kind]}`);
}
return exp;
}

View File

@ -0,0 +1,28 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "test_lib",
testonly = 1,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
],
)

View File

@ -0,0 +1,79 @@
/**
* @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 * as ts from 'typescript';
import {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {NgModuleDecoratorHandler} from '../src/ng_module';
import {SelectorScopeRegistry} from '../src/selector_scope';
describe('SelectorScopeRegistry', () => {
it('absolute imports work', () => {
const {program} = makeProgram([
{
name: 'node_modules/@angular/core/index.d.ts',
contents: `
export interface NgComponentDef<A, B> {}
export interface NgModuleDef<A, B, C, D> {}
`
},
{
name: 'node_modules/some_library/index.d.ts',
contents: `
import {NgComponentDef, NgModuleDef} from '@angular/core';
import * as i0 from './component';
export declare class SomeModule {
static ngModuleDef: NgModuleDef<SomeModule, [i0.SomeCmp], any, [i0.SomeCmp]>;
}
export declare class SomeCmp {
static ngComponentDef: NgComponentDef<SomeCmp, 'some-cmp'>;
}
`
},
{
name: 'node_modules/some_library/component.d.ts',
contents: `
export declare class SomeCmp {}
`
},
{
name: 'entry.ts',
contents: `
export class ProgramCmp {}
export class ProgramModule {}
`
},
]);
const checker = program.getTypeChecker();
const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
const SomeModule = getDeclaration(
program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration);
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const registry = new SelectorScopeRegistry(checker);
registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
exports: [],
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
});
registry.registerSelector(ProgramCmp, 'program-cmp');
const scope = registry.lookupCompilationScope(ProgramCmp) !;
expect(scope).toBeDefined();
expect(scope.directives).toBeDefined();
expect(scope.directives.size).toBe(1);
});
});

View File

@ -10,6 +10,7 @@ ts_library(
]),
module_name = "@angular/compiler-cli/src/ngtsc/metadata",
deps = [
"//packages:types",
"//packages/compiler",
],
)

View File

@ -6,5 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator} from './src/reflector';
export {Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver';
export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator, reflectNonStaticField, reflectObjectLiteral, reflectStaticField, reflectTypeEntityToDeclaration,} from './src/reflector';
export {AbsoluteReference, Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver';

View File

@ -236,3 +236,103 @@ export function reflectImportedIdentifier(
return {from, name};
}
export interface DecoratedNode<T extends ts.Node> {
element: T;
decorators: Decorator[];
}
export function getDecoratedClassElements(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): DecoratedNode<ts.ClassElement>[] {
const decoratedElements: DecoratedNode<ts.ClassElement>[] = [];
clazz.members.forEach(element => {
if (element.decorators !== undefined) {
const decorators = element.decorators.map(decorator => reflectDecorator(decorator, checker))
.filter(decorator => decorator != null) as Decorator[];
if (decorators.length > 0) {
decoratedElements.push({element, decorators});
}
}
});
return decoratedElements;
}
export function reflectStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers === undefined ||
!member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
}
export function reflectNonStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers !== undefined &&
member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
}
export function reflectTypeEntityToDeclaration(
type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string | null} {
let realSymbol = checker.getSymbolAtLocation(type);
if (realSymbol === undefined) {
throw new Error(`Cannot resolve type entity to symbol`);
}
while (realSymbol.flags & ts.SymbolFlags.Alias) {
realSymbol = checker.getAliasedSymbol(realSymbol);
}
let node: ts.Declaration|null = null;
if (realSymbol.valueDeclaration !== undefined) {
node = realSymbol.valueDeclaration;
} else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) {
node = realSymbol.declarations[0];
} else {
throw new Error(`Cannot resolve type entity symbol to declaration`);
}
if (ts.isQualifiedName(type)) {
if (!ts.isIdentifier(type.left)) {
throw new Error(`Cannot handle qualified name with non-identifier lhs`);
}
const symbol = checker.getSymbolAtLocation(type.left);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
throw new Error(`Cannot resolve qualified type entity lhs to symbol`);
}
const decl = symbol.declarations[0];
if (ts.isNamespaceImport(decl)) {
const clause = decl.parent !;
const importDecl = clause.parent !;
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
throw new Error(`Module specifier is not a string`);
}
return {node, from: importDecl.moduleSpecifier.text};
} else {
throw new Error(`Unknown import type?`);
}
} else {
return {node, from: null};
}
}

View File

@ -11,8 +11,12 @@
* values where possible and returning a `DynamicValue` signal when not.
*/
import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
const TS_DTS_EXTENSION = /(\.d)?\.ts$/;
/**
* Represents a value which cannot be determined statically.
*
@ -72,24 +76,99 @@ export interface ResolvedValueArray extends Array<ResolvedValue> {}
*/
type Scope = Map<ts.ParameterDeclaration, ResolvedValue>;
/**
* Whether or not to allow references during resolution.
*
* See `StaticInterpreter` for details.
*/
const enum AllowReferences {
No = 0,
Yes = 1,
}
/**
* A reference to a `ts.Node`.
*
* For example, if an expression evaluates to a function or class definition, it will be returned
* as a `Reference` (assuming references are allowed in evaluation).
*/
export class Reference {
export abstract class Reference {
constructor(readonly node: ts.Node) {}
/**
* Whether an `Expression` can be generated which references the node.
*/
readonly expressable: boolean;
/**
* Generate an `Expression` representing this type, in the context of the given SourceFile.
*
* This could be a local variable reference, if the symbol is imported, or it could be a new
* import if needed.
*/
abstract toExpression(context: ts.SourceFile): Expression|null;
}
/**
* A reference to a node only, without any ability to get an `Expression` representing that node.
*
* This is used for returning references to things like method declarations, which are not directly
* referenceable.
*/
export class NodeReference extends Reference {
toExpression(context: ts.SourceFile): null { return null; }
}
/**
* A reference to a node which has a `ts.Identifier` and can be resolved to an `Expression`.
*
* Imports generated by `ResolvedReference`s are always relative.
*/
export class ResolvedReference extends Reference {
constructor(node: ts.Node, protected identifier: ts.Identifier) { super(node); }
readonly expressable = true;
toExpression(context: ts.SourceFile): Expression {
if (ts.getOriginalNode(context) === ts.getOriginalNode(this.node).getSourceFile()) {
return new WrappedNodeExpr(this.identifier);
} else {
// Relative import from context -> this.node.getSourceFile().
// TODO(alxhub): investigate the impact of multiple source roots here.
// TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support.
let relative =
path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName)
.replace(TS_DTS_EXTENSION, '');
// path.relative() does not include the leading './'.
if (!relative.startsWith('.')) {
relative = `./${relative}`;
}
// path.relative() returns the empty string (converted to './' above) if the two paths are the
// same.
if (relative === './') {
// Same file after all.
return new WrappedNodeExpr(this.identifier);
} else {
return new ExternalExpr(new ExternalReference(relative, this.identifier.text));
}
}
}
}
/**
* A reference to a node which has a `ts.Identifer` and an expected absolute module name.
*
* An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import
* the module specifier will be an absolute module name, not a relative path.
*/
export class AbsoluteReference extends Reference {
constructor(
node: ts.Node, private identifier: ts.Identifier, readonly moduleName: string,
private symbolName: string) {
super(node);
}
readonly expressable = true;
toExpression(context: ts.SourceFile): Expression {
if (ts.getOriginalNode(context) === ts.getOriginalNode(this.node.getSourceFile())) {
return new WrappedNodeExpr(this.identifier);
} else {
return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName));
}
}
}
/**
@ -100,9 +179,8 @@ export class Reference {
* @returns a `ResolvedValue` representing the resolved value
*/
export function staticallyResolve(node: ts.Expression, checker: ts.TypeChecker): ResolvedValue {
return new StaticInterpreter(
checker, new Map<ts.ParameterDeclaration, ResolvedValue>(), AllowReferences.No)
.visit(node);
return new StaticInterpreter(checker).visit(
node, {absoluteModuleName: null, scope: new Map<ts.ParameterDeclaration, ResolvedValue>()});
}
interface BinaryOperatorDef {
@ -144,59 +222,67 @@ const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
[ts.SyntaxKind.PlusToken, a => +a], [ts.SyntaxKind.ExclamationToken, a => !a]
]);
interface Context {
absoluteModuleName: string|null;
scope: Scope;
}
class StaticInterpreter {
constructor(
private checker: ts.TypeChecker, private scope: Scope,
private allowReferences: AllowReferences) {}
constructor(private checker: ts.TypeChecker) {}
visit(node: ts.Expression): ResolvedValue { return this.visitExpression(node); }
visit(node: ts.Expression, context: Context): ResolvedValue {
return this.visitExpression(node, context);
}
private visitExpression(node: ts.Expression): ResolvedValue {
private visitExpression(node: ts.Expression, context: Context): ResolvedValue {
if (node.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
return false;
} else if (ts.isStringLiteral(node)) {
return node.text;
} else if (ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
} else if (ts.isNumericLiteral(node)) {
return parseFloat(node.text);
} else if (ts.isObjectLiteralExpression(node)) {
return this.visitObjectLiteralExpression(node);
return this.visitObjectLiteralExpression(node, context);
} else if (ts.isIdentifier(node)) {
return this.visitIdentifier(node);
return this.visitIdentifier(node, context);
} else if (ts.isPropertyAccessExpression(node)) {
return this.visitPropertyAccessExpression(node);
return this.visitPropertyAccessExpression(node, context);
} else if (ts.isCallExpression(node)) {
return this.visitCallExpression(node);
return this.visitCallExpression(node, context);
} else if (ts.isConditionalExpression(node)) {
return this.visitConditionalExpression(node);
return this.visitConditionalExpression(node, context);
} else if (ts.isPrefixUnaryExpression(node)) {
return this.visitPrefixUnaryExpression(node);
return this.visitPrefixUnaryExpression(node, context);
} else if (ts.isBinaryExpression(node)) {
return this.visitBinaryExpression(node);
return this.visitBinaryExpression(node, context);
} else if (ts.isArrayLiteralExpression(node)) {
return this.visitArrayLiteralExpression(node);
return this.visitArrayLiteralExpression(node, context);
} else if (ts.isParenthesizedExpression(node)) {
return this.visitParenthesizedExpression(node);
return this.visitParenthesizedExpression(node, context);
} else if (ts.isElementAccessExpression(node)) {
return this.visitElementAccessExpression(node);
return this.visitElementAccessExpression(node, context);
} else if (ts.isAsExpression(node)) {
return this.visitExpression(node.expression);
return this.visitExpression(node.expression, context);
} else if (ts.isNonNullExpression(node)) {
return this.visitExpression(node.expression);
return this.visitExpression(node.expression, context);
} else if (ts.isClassDeclaration(node)) {
return this.visitDeclaration(node);
return this.visitDeclaration(node, context);
} else {
return DYNAMIC_VALUE;
}
}
private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression): ResolvedValue {
private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression, context: Context):
ResolvedValue {
const array: ResolvedValueArray = [];
for (let i = 0; i < node.elements.length; i++) {
const element = node.elements[i];
if (ts.isSpreadElement(element)) {
const spread = this.visitExpression(element.expression);
const spread = this.visitExpression(element.expression, context);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
@ -206,7 +292,7 @@ class StaticInterpreter {
array.push(...spread);
} else {
const result = this.visitExpression(element);
const result = this.visitExpression(element, context);
if (isDynamicValue(result)) {
return DYNAMIC_VALUE;
}
@ -217,27 +303,28 @@ class StaticInterpreter {
return array;
}
private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression): ResolvedValue {
private visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: Context):
ResolvedValue {
const map: ResolvedValueMap = new Map<string, ResolvedValue>();
for (let i = 0; i < node.properties.length; i++) {
const property = node.properties[i];
if (ts.isPropertyAssignment(property)) {
const name = this.stringNameFromPropertyName(property.name);
const name = this.stringNameFromPropertyName(property.name, context);
// Check whether the name can be determined statically.
if (name === undefined) {
return DYNAMIC_VALUE;
}
map.set(name, this.visitExpression(property.initializer));
map.set(name, this.visitExpression(property.initializer, context));
} else if (ts.isShorthandPropertyAssignment(property)) {
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
if (symbol === undefined || symbol.valueDeclaration === undefined) {
return DYNAMIC_VALUE;
}
map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration));
map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration, context));
} else if (ts.isSpreadAssignment(property)) {
const spread = this.visitExpression(property.expression);
const spread = this.visitExpression(property.expression, context);
if (isDynamicValue(spread)) {
return DYNAMIC_VALUE;
}
@ -252,19 +339,34 @@ class StaticInterpreter {
return map;
}
private visitIdentifier(node: ts.Identifier): ResolvedValue {
private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue {
let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(node);
if (symbol === undefined) {
return DYNAMIC_VALUE;
}
const result = this.visitSymbol(symbol);
if (this.allowReferences === AllowReferences.Yes && isDynamicValue(result)) {
return new Reference(node);
}
return result;
return this.visitSymbol(symbol, context);
}
private visitSymbol(symbol: ts.Symbol): ResolvedValue {
private visitSymbol(symbol: ts.Symbol, context: Context): ResolvedValue {
let absoluteModuleName = context.absoluteModuleName;
if (symbol.declarations !== undefined && symbol.declarations.length > 0) {
for (let i = 0; i < symbol.declarations.length; i++) {
const decl = symbol.declarations[i];
if (ts.isImportSpecifier(decl) && decl.parent !== undefined &&
decl.parent.parent !== undefined && decl.parent.parent.parent !== undefined) {
const importDecl = decl.parent.parent.parent;
if (ts.isStringLiteral(importDecl.moduleSpecifier)) {
const moduleSpecifier = importDecl.moduleSpecifier.text;
if (!moduleSpecifier.startsWith('.')) {
absoluteModuleName = moduleSpecifier;
}
}
}
}
}
const newContext = {...context, absoluteModuleName};
while (symbol.flags & ts.SymbolFlags.Alias) {
symbol = this.checker.getAliasedSymbol(symbol);
}
@ -274,42 +376,44 @@ class StaticInterpreter {
}
if (symbol.valueDeclaration !== undefined) {
return this.visitDeclaration(symbol.valueDeclaration);
return this.visitDeclaration(symbol.valueDeclaration, newContext);
}
return symbol.declarations.reduce<ResolvedValue>((prev, decl) => {
if (!(isDynamicValue(prev) || prev instanceof Reference)) {
return prev;
}
return this.visitDeclaration(decl);
return this.visitDeclaration(decl, newContext);
}, DYNAMIC_VALUE);
}
private visitDeclaration(node: ts.Declaration): ResolvedValue {
private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue {
if (ts.isVariableDeclaration(node)) {
if (!node.initializer) {
return undefined;
}
return this.visitExpression(node.initializer);
} else if (ts.isParameter(node) && this.scope.has(node)) {
return this.scope.get(node) !;
return this.visitExpression(node.initializer, context);
} else if (ts.isParameter(node) && context.scope.has(node)) {
return context.scope.get(node) !;
} else if (ts.isExportAssignment(node)) {
return this.visitExpression(node.expression);
return this.visitExpression(node.expression, context);
} else if (ts.isSourceFile(node)) {
return this.visitSourceFile(node);
return this.visitSourceFile(node, context);
} else {
return this.getReference(node, context);
}
return this.allowReferences === AllowReferences.Yes ? new Reference(node) : DYNAMIC_VALUE;
}
private visitElementAccessExpression(node: ts.ElementAccessExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
private visitElementAccessExpression(node: ts.ElementAccessExpression, context: Context):
ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
if (node.argumentExpression === undefined) {
throw new Error(`Expected argument in ElementAccessExpression`);
}
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
const rhs = this.withNoReferences.visitExpression(node.argumentExpression);
const rhs = this.visitExpression(node.argumentExpression, context);
if (isDynamicValue(rhs)) {
return DYNAMIC_VALUE;
}
@ -318,33 +422,34 @@ class StaticInterpreter {
`ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`);
}
return this.accessHelper(lhs, rhs);
return this.accessHelper(lhs, rhs, context);
}
private visitPropertyAccessExpression(node: ts.PropertyAccessExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context):
ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
const rhs = node.name.text;
// TODO: handle reference to class declaration.
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
return this.accessHelper(lhs, rhs);
return this.accessHelper(lhs, rhs, context);
}
private visitSourceFile(node: ts.SourceFile): ResolvedValue {
private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue {
const map = new Map<string, ResolvedValue>();
const symbol = this.checker.getSymbolAtLocation(node);
if (symbol === undefined) {
return DYNAMIC_VALUE;
}
const exports = this.checker.getExportsOfModule(symbol);
exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol)));
exports.forEach(symbol => map.set(symbol.name, this.visitSymbol(symbol, context)));
return map;
}
private accessHelper(lhs: ResolvedValue, rhs: string|number): ResolvedValue {
private accessHelper(lhs: ResolvedValue, rhs: string|number, context: Context): ResolvedValue {
const strIndex = `${rhs}`;
if (lhs instanceof Map) {
if (lhs.has(strIndex)) {
@ -367,16 +472,16 @@ class StaticInterpreter {
const ref = lhs.node;
if (ts.isClassDeclaration(ref)) {
let value: ResolvedValue = undefined;
const member = ref.members.filter(member => isStatic(member))
.find(
member => member.name !== undefined &&
this.stringNameFromPropertyName(member.name) === strIndex);
const member =
ref.members.filter(member => isStatic(member))
.find(
member => member.name !== undefined &&
this.stringNameFromPropertyName(member.name, context) === strIndex);
if (member !== undefined) {
if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) {
value = this.visitExpression(member.initializer);
value = this.visitExpression(member.initializer, context);
} else if (ts.isMethodDeclaration(member)) {
value = this.allowReferences === AllowReferences.Yes ? new Reference(member) :
DYNAMIC_VALUE;
value = new NodeReference(member);
}
}
return value;
@ -385,8 +490,8 @@ class StaticInterpreter {
throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`);
}
private visitCallExpression(node: ts.CallExpression): ResolvedValue {
const lhs = this.withReferences.visitExpression(node.expression);
private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue {
const lhs = this.visitExpression(node.expression, context);
if (!(lhs instanceof Reference)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`);
} else if (!isFunctionOrMethodDeclaration(lhs.node) || !lhs.node.body) {
@ -406,43 +511,46 @@ class StaticInterpreter {
let value: ResolvedValue = undefined;
if (index < node.arguments.length) {
const arg = node.arguments[index];
value = this.visitExpression(arg);
value = this.visitExpression(arg, context);
}
if (value === undefined && param.initializer !== undefined) {
value = this.visitExpression(param.initializer);
value = this.visitExpression(param.initializer, context);
}
newScope.set(param, value);
});
return ret.expression !== undefined ? this.withScope(newScope).visitExpression(ret.expression) :
undefined;
return ret.expression !== undefined ?
this.visitExpression(ret.expression, {...context, scope: newScope}) :
undefined;
}
private visitConditionalExpression(node: ts.ConditionalExpression): ResolvedValue {
const condition = this.withNoReferences.visitExpression(node.condition);
private visitConditionalExpression(node: ts.ConditionalExpression, context: Context):
ResolvedValue {
const condition = this.visitExpression(node.condition, context);
if (isDynamicValue(condition)) {
return condition;
}
if (condition) {
return this.visitExpression(node.whenTrue);
return this.visitExpression(node.whenTrue, context);
} else {
return this.visitExpression(node.whenFalse);
return this.visitExpression(node.whenFalse, context);
}
}
private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ResolvedValue {
private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression, context: Context):
ResolvedValue {
const operatorKind = node.operator;
if (!UNARY_OPERATORS.has(operatorKind)) {
throw new Error(`Unsupported prefix unary operator: ${ts.SyntaxKind[operatorKind]}`);
}
const op = UNARY_OPERATORS.get(operatorKind) !;
const value = this.visitExpression(node.operand);
const value = this.visitExpression(node.operand, context);
return isDynamicValue(value) ? DYNAMIC_VALUE : op(value);
}
private visitBinaryExpression(node: ts.BinaryExpression): ResolvedValue {
private visitBinaryExpression(node: ts.BinaryExpression, context: Context): ResolvedValue {
const tokenKind = node.operatorToken.kind;
if (!BINARY_OPERATORS.has(tokenKind)) {
throw new Error(`Unsupported binary operator: ${ts.SyntaxKind[tokenKind]}`);
@ -451,44 +559,42 @@ class StaticInterpreter {
const opRecord = BINARY_OPERATORS.get(tokenKind) !;
let lhs: ResolvedValue, rhs: ResolvedValue;
if (opRecord.literal) {
const withNoReferences = this.withNoReferences;
lhs = literal(withNoReferences.visitExpression(node.left));
rhs = literal(withNoReferences.visitExpression(node.right));
lhs = literal(this.visitExpression(node.left, context));
rhs = literal(this.visitExpression(node.right, context));
} else {
lhs = this.visitExpression(node.left);
rhs = this.visitExpression(node.right);
lhs = this.visitExpression(node.left, context);
rhs = this.visitExpression(node.right, context);
}
return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs);
}
private visitParenthesizedExpression(node: ts.ParenthesizedExpression): ResolvedValue {
return this.visitExpression(node.expression);
private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context):
ResolvedValue {
return this.visitExpression(node.expression, context);
}
private stringNameFromPropertyName(node: ts.PropertyName): string|undefined {
private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string|undefined {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else { // ts.ComputedPropertyName
const literal = this.withNoReferences.visitExpression(node.expression);
const literal = this.visitExpression(node.expression, context);
return typeof literal === 'string' ? literal : undefined;
}
}
private get withReferences(): StaticInterpreter {
return this.allowReferences === AllowReferences.Yes ?
this :
new StaticInterpreter(this.checker, this.scope, AllowReferences.Yes);
}
private get withNoReferences(): StaticInterpreter {
return this.allowReferences === AllowReferences.No ?
this :
new StaticInterpreter(this.checker, this.scope, AllowReferences.No);
}
private withScope(scope: Scope): StaticInterpreter {
return new StaticInterpreter(this.checker, scope, this.allowReferences);
private getReference(node: ts.Declaration, context: Context): Reference {
const id = identifierOfDeclaration(node);
if (id === undefined) {
throw new Error(`Don't know how to refer to ${ts.SyntaxKind[node.kind]}`);
}
if (context.absoluteModuleName !== null) {
// TODO(alxhub): investigate whether this can get symbol names wrong in the event of
// re-exports under different names.
return new AbsoluteReference(node, id, context.absoluteModuleName, id.text);
} else {
return new ResolvedReference(node, id);
}
}
}
@ -512,3 +618,17 @@ function literal(value: ResolvedValue): any {
}
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
}
function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined {
if (ts.isClassDeclaration(decl)) {
return decl.name;
} else if (ts.isFunctionDeclaration(decl)) {
return decl.name;
} else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
return decl.name;
} else if (ts.isShorthandPropertyAssignment(decl)) {
return decl.name;
} else {
return undefined;
}
}

View File

@ -11,6 +11,7 @@ ts_library(
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
],

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {ResolvedValue, staticallyResolve} from '../src/resolver';
import {Reference, ResolvedValue, staticallyResolve} from '../src/resolver';
function makeSimpleProgram(contents: string): ts.Program {
return makeProgram([{name: 'entry.ts', contents}]).program;
@ -116,6 +117,62 @@ describe('ngtsc metadata', () => {
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
});
it('imports work', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
{
name: 'entry.ts',
contents: `
import {foo} from './second';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof ExternalExpr)) {
return fail('Expected expression reference to be an external (import) expression');
}
expect(reference.value.moduleName).toBe('./second');
expect(reference.value.name).toBe('foo');
});
it('absolute imports work', () => {
const {program} = makeProgram([
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
{
name: 'entry.ts',
contents: `
import {foo} from 'some_library';
const target$ = foo;
`
},
]);
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
const resolved = staticallyResolve(expr, checker);
if (!(resolved instanceof Reference)) {
return fail('Expected expression to resolve to a reference');
}
expect(ts.isFunctionDeclaration(resolved.node)).toBe(true);
expect(resolved.expressable).toBe(true);
const reference = resolved.toExpression(program.getSourceFile('entry.ts') !);
if (!(reference instanceof ExternalExpr)) {
return fail('Expected expression reference to be an external (import) expression');
}
expect(reference.value.moduleName).toBe('some_library');
expect(reference.value.name).toBe('foo');
});
it('reads values from default exports', () => {
const {program} = makeProgram([
{name: 'second.ts', contents: 'export default {property: "test"}'},
@ -130,7 +187,6 @@ describe('ngtsc metadata', () => {
const checker = program.getTypeChecker();
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !;
debugger;
expect(staticallyResolve(expr, checker)).toEqual('test');
});

View File

@ -12,8 +12,9 @@ import * as ts from 'typescript';
import * as api from '../transformers/api';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, SelectorScopeRegistry} from './annotations';
import {CompilerHost} from './compiler_host';
import {InjectableCompilerAdapter, IvyCompilation, ivyTransformFactory} from './transform';
import {IvyCompilation, ivyTransformFactory} from './transform';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
@ -89,10 +90,16 @@ export class NgtscProgram implements api.Program {
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
const checker = this.tsProgram.getTypeChecker();
const scopeRegistry = new SelectorScopeRegistry(checker);
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const adapters = [new InjectableCompilerAdapter(checker)];
const compilation = new IvyCompilation(adapters, checker);
const handlers = [
new ComponentDecoratorHandler(checker, scopeRegistry),
new DirectiveDecoratorHandler(checker, scopeRegistry),
new InjectableDecoratorHandler(checker),
new NgModuleDecoratorHandler(checker, scopeRegistry),
];
const compilation = new IvyCompilation(handlers, checker);
// Analyze every source file in the program.
this.tsProgram.getSourceFiles()

View File

@ -15,7 +15,10 @@ export function makeProgram(files: {name: string, contents: string}[]):
files.forEach(file => host.writeFile(file.name, file.contents));
const rootNames = files.map(file => host.getCanonicalFileName(file.name));
const program = ts.createProgram(rootNames, {noLib: true, experimentalDecorators: true}, host);
const program = ts.createProgram(
rootNames,
{noLib: true, experimentalDecorators: true, moduleResolution: ts.ModuleResolutionKind.NodeJs},
host);
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
throw new Error(

View File

@ -12,5 +12,6 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/util",
],
)

View File

@ -6,6 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
export * from './src/api';
export {IvyCompilation} from './src/compilation';
export {InjectableCompilerAdapter} from './src/injectable';
export {ivyTransformFactory} from './src/transform';

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, Type} from '@angular/compiler';
import {Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
@ -15,13 +15,13 @@ import {Decorator} from '../../metadata';
* Provides the interface between a decorator compiler from @angular/compiler and the Typescript
* compiler/transform.
*
* The decorator compilers in @angular/compiler do not depend on Typescript. The adapter is
* The decorator compilers in @angular/compiler do not depend on Typescript. The handler is
* responsible for extracting the information required to perform compilation from the decorators
* and Typescript source, invoking the decorator compiler, and returning the result.
*/
export interface CompilerAdapter<A> {
export interface DecoratorHandler<A> {
/**
* Scan a set of reflected decorators and determine if this adapter is responsible for compilation
* Scan a set of reflected decorators and determine if this handler is responsible for compilation
* of one of them.
*/
detect(decorator: Decorator[]): Decorator|undefined;
@ -37,7 +37,7 @@ export interface CompilerAdapter<A> {
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
*/
compile(node: ts.ClassDeclaration, analysis: A): AddStaticFieldInstruction;
compile(node: ts.ClassDeclaration, analysis: A): CompileResult;
}
/**
@ -54,8 +54,9 @@ export interface AnalysisOutput<A> {
* A description of the static field to add to a class, including an initialization expression
* and a type for the .d.ts file.
*/
export interface AddStaticFieldInstruction {
export interface CompileResult {
field: string;
initializer: Expression;
statements: Statement[];
type: Type;
}

View File

@ -11,17 +11,18 @@ import * as ts from 'typescript';
import {Decorator, reflectDecorator} from '../../metadata';
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
import {AnalysisOutput, CompileResult, DecoratorHandler} from './api';
import {DtsFileTransformer} from './declaration';
import {ImportManager, translateType} from './translator';
/**
* Record of an adapter which decided to emit a static field, and the analysis it performed to
* prepare for that operation.
*/
interface EmitFieldOperation<T> {
adapter: CompilerAdapter<T>;
adapter: DecoratorHandler<T>;
analysis: AnalysisOutput<T>;
decorator: ts.Decorator;
}
@ -44,7 +45,7 @@ export class IvyCompilation {
*/
private dtsMap = new Map<string, DtsFileTransformer>();
constructor(private adapters: CompilerAdapter<any>[], private checker: ts.TypeChecker) {}
constructor(private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker) {}
/**
* Analyze a source file and produce diagnostics for it (if any).
@ -60,8 +61,8 @@ export class IvyCompilation {
node.decorators.map(decorator => reflectDecorator(decorator, this.checker))
.filter(decorator => decorator !== null) as Decorator[];
// Look through the CompilerAdapters to see if any are relevant.
this.adapters.forEach(adapter => {
// Look through the DecoratorHandlers to see if any are relevant.
this.handlers.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators);
if (decorator === undefined) {
@ -101,7 +102,7 @@ export class IvyCompilation {
* Perform a compilation operation on the given class declaration and return instructions to an
* AST transformer if any are available.
*/
compileIvyFieldFor(node: ts.ClassDeclaration): AddStaticFieldInstruction|undefined {
compileIvyFieldFor(node: ts.ClassDeclaration): CompileResult|undefined {
// Look to see whether the original node was analyzed. If not, there's nothing to do.
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
if (!this.analysis.has(original)) {

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {AddStaticFieldInstruction} from './api';
import {CompileResult} from './api';
import {ImportManager, translateType} from './translator';
@ -17,15 +17,13 @@ import {ImportManager, translateType} from './translator';
* Processes .d.ts file text and adds static field declarations, with types.
*/
export class DtsFileTransformer {
private ivyFields = new Map<string, AddStaticFieldInstruction>();
private ivyFields = new Map<string, CompileResult>();
private imports = new ImportManager();
/**
* Track that a static field was added to the code for a class.
*/
recordStaticField(name: string, decl: AddStaticFieldInstruction): void {
this.ivyFields.set(name, decl);
}
recordStaticField(name: string, decl: CompileResult): void { this.ivyFields.set(name, decl); }
/**
* Process the .d.ts text for a file and add any declarations which were recorded.

View File

@ -9,8 +9,10 @@
import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor';
import {IvyCompilation} from './compilation';
import {ImportManager, translateExpression} from './translator';
import {ImportManager, translateExpression, translateStatement} from './translator';
export function ivyTransformFactory(compilation: IvyCompilation):
ts.TransformerFactory<ts.SourceFile> {
@ -21,6 +23,40 @@ export function ivyTransformFactory(compilation: IvyCompilation):
};
}
class IvyVisitor extends Visitor {
constructor(private compilation: IvyCompilation, private importManager: ImportManager) {
super();
}
visitClassDeclaration(node: ts.ClassDeclaration):
VisitListEntryResult<ts.Statement, ts.ClassDeclaration> {
// Determine if this class has an Ivy field that needs to be added, and compile the field
// to an expression if so.
const res = this.compilation.compileIvyFieldFor(node);
if (res !== undefined) {
// There is a field to add. Translate the initializer for the field into TS nodes.
const exprNode = translateExpression(res.initializer, this.importManager);
// Create a static property declaration for the new field.
const property = ts.createProperty(
undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined,
exprNode);
// Replace the class declaration with an updated version.
node = ts.updateClassDeclaration(
node,
// Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator(node.decorators, this.compilation.ivyDecoratorFor(node) !),
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
[...node.members, property]);
const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager));
return {node, before: statements};
}
return {node};
}
}
/**
* A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
*/
@ -30,7 +66,7 @@ function transformIvySourceFile(
const importManager = new ImportManager();
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
const sf = visitNode(file);
const sf = visit(file, new IvyVisitor(compilation, importManager), context);
// Generate the import statements to prepend.
const imports = importManager.getAllImports().map(
@ -44,44 +80,8 @@ function transformIvySourceFile(
sf.statements = ts.createNodeArray([...imports, ...sf.statements]);
}
return sf;
// Helper function to process a class declaration.
function visitClassDeclaration(node: ts.ClassDeclaration): ts.ClassDeclaration {
// Determine if this class has an Ivy field that needs to be added, and compile the field
// to an expression if so.
const res = compilation.compileIvyFieldFor(node);
if (res !== undefined) {
// There is a field to add. Translate the initializer for the field into TS nodes.
const exprNode = translateExpression(res.initializer, importManager);
// Create a static property declaration for the new field.
const property = ts.createProperty(
undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined,
exprNode);
// Replace the class declaration with an updated version.
node = ts.updateClassDeclaration(
node,
// Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator(node.decorators, compilation.ivyDecoratorFor(node) !),
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
[...node.members, property]);
}
// Recurse into the class declaration in case there are nested class declarations.
return ts.visitEachChild(node, child => visitNode(child), context);
}
// Helper function that recurses through the nodes and processes each one.
function visitNode<T extends ts.Node>(node: T): T;
function visitNode(node: ts.Node): ts.Node {
if (ts.isClassDeclaration(node)) {
return visitClassDeclaration(node);
} else {
return ts.visitEachChild(node, child => visitNode(child), context);
}
}
}
function maybeFilterDecorator(
decorators: ts.NodeArray<ts.Decorator>| undefined,
toRemove: ts.Decorator): ts.NodeArray<ts.Decorator>|undefined {

View File

@ -6,9 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ArrayType, AssertNotNull, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
[BinaryOperator.BiggerEquals, ts.SyntaxKind.GreaterThanEqualsToken],
[BinaryOperator.BitwiseAnd, ts.SyntaxKind.AmpersandToken],
[BinaryOperator.Divide, ts.SyntaxKind.SlashToken],
[BinaryOperator.Equals, ts.SyntaxKind.EqualsEqualsToken],
[BinaryOperator.Identical, ts.SyntaxKind.EqualsEqualsEqualsToken],
[BinaryOperator.Lower, ts.SyntaxKind.LessThanToken],
[BinaryOperator.LowerEquals, ts.SyntaxKind.LessThanEqualsToken],
[BinaryOperator.Minus, ts.SyntaxKind.MinusToken],
[BinaryOperator.Modulo, ts.SyntaxKind.PercentToken],
[BinaryOperator.Multiply, ts.SyntaxKind.AsteriskToken],
[BinaryOperator.NotEquals, ts.SyntaxKind.ExclamationEqualsToken],
[BinaryOperator.NotIdentical, ts.SyntaxKind.ExclamationEqualsEqualsToken],
[BinaryOperator.Or, ts.SyntaxKind.BarBarToken],
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
]);
export class ImportManager {
private moduleToIndex = new Map<string, string>();
private nextIndex = 0;
@ -32,6 +51,10 @@ export function translateExpression(expression: Expression, imports: ImportManag
return expression.visitExpression(new ExpressionTranslatorVisitor(imports), null);
}
export function translateStatement(statement: Statement, imports: ImportManager): ts.Statement {
return statement.visitStatement(new ExpressionTranslatorVisitor(imports), null);
}
export function translateType(type: Type, imports: ImportManager): string {
return type.visitType(new TypeTranslatorVisitor(imports), null);
}
@ -39,16 +62,23 @@ export function translateType(type: Type, imports: ImportManager): string {
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
constructor(private imports: ImportManager) {}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any) {
throw new Error('Method not implemented.');
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): ts.VariableStatement {
return ts.createVariableStatement(
undefined,
ts.createVariableDeclarationList([ts.createVariableDeclaration(
stmt.name, undefined, stmt.value && stmt.value.visitExpression(this, context))]));
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) {
throw new Error('Method not implemented.');
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): ts.FunctionDeclaration {
return ts.createFunctionDeclaration(
undefined, undefined, undefined, stmt.name, undefined,
stmt.params.map(param => ts.createParameter(undefined, undefined, undefined, param.name)),
undefined,
ts.createBlock(stmt.statements.map(child => child.visitStatement(this, context))));
}
visitExpressionStmt(stmt: ExpressionStatement, context: any) {
throw new Error('Method not implemented.');
visitExpressionStmt(stmt: ExpressionStatement, context: any): ts.ExpressionStatement {
return ts.createStatement(stmt.expr.visitExpression(this, context));
}
visitReturnStmt(stmt: ReturnStatement, context: any): ts.ReturnStatement {
@ -59,7 +89,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
throw new Error('Method not implemented.');
}
visitIfStmt(stmt: IfStmt, context: any) { throw new Error('Method not implemented.'); }
visitIfStmt(stmt: IfStmt, context: any): ts.IfStatement {
return ts.createIf(
stmt.condition.visitExpression(this, context),
ts.createBlock(stmt.trueCase.map(child => child.visitStatement(this, context))),
stmt.falseCase.length > 0 ?
ts.createBlock(stmt.falseCase.map(child => child.visitStatement(this, context))) :
undefined);
}
visitTryCatchStmt(stmt: TryCatchStmt, context: any) {
throw new Error('Method not implemented.');
@ -89,12 +126,17 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
throw new Error('Method not implemented.');
}
visitWritePropExpr(expr: WritePropExpr, context: any): never {
throw new Error('Method not implemented.');
visitWritePropExpr(expr: WritePropExpr, context: any): ts.BinaryExpression {
return ts.createBinary(
ts.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name),
ts.SyntaxKind.EqualsToken, expr.value.visitExpression(this, context));
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
throw new Error('Method not implemented.');
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): ts.CallExpression {
const target = ast.receiver.visitExpression(this, context);
return ts.createCall(
ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): ts.CallExpression {
@ -156,20 +198,26 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): never {
throw new Error('Method not implemented.');
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): ts.Expression {
if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
}
const binEx = ts.createBinary(
ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator) !,
ast.rhs.visitExpression(this, context));
return ast.parens ? ts.createParen(binEx) : binEx;
}
visitReadPropExpr(ast: ReadPropExpr, context: any): never {
throw new Error('Method not implemented.');
visitReadPropExpr(ast: ReadPropExpr, context: any): ts.PropertyAccessExpression {
return ts.createPropertyAccess(ast.receiver.visitExpression(this, context), ast.name);
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): never {
throw new Error('Method not implemented.');
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): never {
throw new Error('Method not implemented.');
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): ts.ArrayLiteralExpression {
return ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context)));
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): ts.ObjectLiteralExpression {
@ -297,8 +345,9 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
visitReadKeyExpr(ast: ReadKeyExpr, context: any) { throw new Error('Method not implemented.'); }
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any) {
throw new Error('Method not implemented.');
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): string {
const values = ast.entries.map(expr => expr.visitExpression(this, context));
return `[${values.join(',')}]`;
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any) {

View File

@ -33,7 +33,7 @@ export abstract class Visitor {
/**
* Maps statements to an array of statements that should be inserted before them.
*/
private _before = new Map<ts.Statement, ts.Statement[]>();
private _before = new Map<ts.Node, ts.Statement[]>();
/**
* Visit a class declaration, returning at least the transformed declaration and optionally other
@ -44,16 +44,15 @@ export abstract class Visitor {
return {node};
}
private _visitClassDeclaration(node: ts.ClassDeclaration, context: ts.TransformationContext):
ts.ClassDeclaration {
const result = this.visitClassDeclaration(node);
const visited = ts.visitEachChild(result.node, child => this._visit(child, context), context);
private _visitListEntryNode<T extends ts.Statement>(
node: T, visitor: (node: T) => VisitListEntryResult<ts.Statement, T>): T {
const result = visitor(node);
if (result.before !== undefined) {
// Record that some nodes should be inserted before the given declaration. The declaration's
// parent's _visit call is responsible for performing this insertion.
this._before.set(visited, result.before);
this._before.set(result.node, result.before);
}
return visited;
return result.node;
}
/**
@ -61,11 +60,6 @@ export abstract class Visitor {
*/
visitOtherNode<T extends ts.Node>(node: T): T { return node; }
private _visitOtherNode<T extends ts.Node>(node: T, context: ts.TransformationContext): T {
return ts.visitEachChild(
this.visitOtherNode(node), child => this._visit(child, context), context);
}
/**
* @internal
*/
@ -73,10 +67,14 @@ export abstract class Visitor {
// First, visit the node. visitedNode starts off as `null` but should be set after visiting
// is completed.
let visitedNode: T|null = null;
node = ts.visitEachChild(node, child => this._visit(child, context), context) as T;
if (ts.isClassDeclaration(node)) {
visitedNode = this._visitClassDeclaration(node, context) as typeof node;
visitedNode = this._visitListEntryNode(
node, (node: ts.ClassDeclaration) => this.visitClassDeclaration(node)) as typeof node;
} else {
visitedNode = this._visitOtherNode(node, context);
visitedNode = this.visitOtherNode(node);
}
// If the visited node has a `statements` array then process them, maybe replacing the visited

View File

@ -16,6 +16,8 @@ function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
return null !;
}
export const Component = callableClassDecorator();
export const Directive = callableClassDecorator();
export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator();

View File

@ -95,7 +95,7 @@ describe('ngtsc behavioral tests', () => {
}`);
});
it('should compile without errors', () => {
it('should compile Injectables without errors', () => {
writeConfig();
write('test.ts', `
import {Injectable} from '@angular/core';
@ -122,4 +122,62 @@ describe('ngtsc behavioral tests', () => {
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Dep>;');
expect(dtsContents).toContain('static ngInjectableDef: i0.InjectableDef<Service>;');
});
it('should compile Components without errors', () => {
writeConfig();
write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: 'this is a test',
})
export class TestCmp {}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵdefineComponent');
expect(jsContents).not.toContain('__decorate');
const dtsContents = getContents('test.d.ts');
expect(dtsContents).toContain('static ngComponentDef: i0.ComponentDef<TestCmp, \'test-cmp\'>');
});
it('should compile NgModules without errors', () => {
writeConfig();
write('test.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'test-cmp',
template: 'this is a test',
})
export class TestCmp {}
@NgModule({
declarations: [TestCmp],
})
export class TestModule {}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents)
.toContain(
'i0.ɵdefineNgModule({ type: TestModule, bootstrap: [], ' +
'declarations: [TestCmp], imports: [], exports: [] })');
const dtsContents = getContents('test.d.ts');
expect(dtsContents).toContain('static ngComponentDef: i0.ComponentDef<TestCmp, \'test-cmp\'>');
expect(dtsContents)
.toContain('static ngModuleDef: i0.NgModuleDef<TestModule, [TestCmp], [], []>');
expect(dtsContents).not.toContain('__decorate');
});
});