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:

committed by
Miško Hevery

parent
0f7e4fae20
commit
27bc7dcb43
@ -25,6 +25,7 @@ ts_library(
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/annotations",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
],
|
||||
)
|
||||
|
17
packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel
Normal file
17
packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
13
packages/compiler-cli/src/ngtsc/annotations/index.ts
Normal file
13
packages/compiler-cli/src/ngtsc/annotations/index.ts
Normal 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';
|
117
packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Normal file
117
packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
212
packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Normal file
212
packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Normal 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});
|
||||
}
|
@ -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 = {
|
116
packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
Normal file
116
packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
Normal 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;
|
||||
}
|
@ -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};
|
||||
}
|
84
packages/compiler-cli/src/ngtsc/annotations/src/util.ts
Normal file
84
packages/compiler-cli/src/ngtsc/annotations/src/util.ts
Normal 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;
|
||||
}
|
28
packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel
Normal file
28
packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
@ -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);
|
||||
});
|
||||
});
|
@ -10,6 +10,7 @@ ts_library(
|
||||
]),
|
||||
module_name = "@angular/compiler-cli/src/ngtsc/metadata",
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
],
|
||||
)
|
||||
|
@ -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';
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ ts_library(
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
],
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -12,5 +12,6 @@ ts_library(
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
],
|
||||
)
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user