diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index fcdf1eb025..cd99c5f7df 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -26,6 +26,7 @@ ts_library( deps = [ "//packages/compiler", "//packages/compiler-cli/src/ngtsc/annotations", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/transform", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 0e502d94c4..4a0cd4bb59 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( module_name = "@angular/compiler-cli/src/ngtsc/annotations", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/transform", ], diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 13894bdf82..e539ef1dc1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -9,11 +9,13 @@ 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 {Decorator, ReflectionHost} from '../../host'; +import {reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {extractDirectiveMetadata} from './directive'; import {SelectorScopeRegistry} from './selector_scope'; +import {isAngularCore} from './util'; const EMPTY_MAP = new Map(); @@ -21,14 +23,18 @@ const EMPTY_MAP = new Map(); * `DecoratorHandler` which handles the `@Component` annotation. */ export class ComponentDecoratorHandler implements DecoratorHandler { - constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} + constructor( + private checker: ts.TypeChecker, private reflector: ReflectionHost, + private scopeRegistry: SelectorScopeRegistry) {} detect(decorators: Decorator[]): Decorator|undefined { - return decorators.find( - decorator => decorator.name === 'Component' && decorator.from === '@angular/core'); + return decorators.find(decorator => decorator.name === 'Component' && isAngularCore(decorator)); } analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + if (decorator.args === null || decorator.args.length !== 1) { + throw new Error(`Incorrect number of arguments to @Component decorator`); + } const meta = decorator.args[0]; if (!ts.isObjectLiteralExpression(meta)) { throw new Error(`Decorator argument must be literal.`); @@ -36,7 +42,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler { - constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} + constructor( + private checker: ts.TypeChecker, private reflector: ReflectionHost, + private scopeRegistry: SelectorScopeRegistry) {} detect(decorators: Decorator[]): Decorator|undefined { - return decorators.find( - decorator => decorator.name === 'Directive' && decorator.from === '@angular/core'); + return decorators.find(decorator => decorator.name === 'Directive' && isAngularCore(decorator)); } analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { - const analysis = extractDirectiveMetadata(node, decorator, this.checker); + const analysis = extractDirectiveMetadata(node, decorator, this.checker, this.reflector); // 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. @@ -54,8 +56,11 @@ export class DirectiveDecoratorHandler implements DecoratorHandler !member.isStatic && member.decorators !== null); - // Construct the map of inputs both from the @Directive/@Component decorator, and the decorated + // 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); + filterToMembersWithDecorator(decoratedElements, 'Input', '@angular/core'), checker); // And outputs. const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker); const outputsFromFields = parseDecoratedFields( - findDecoratedFields(decoratedElements, '@angular/core', 'Output'), checker); + filterToMembersWithDecorator(decoratedElements, '@angular/core', 'Output'), checker); // Parse the selector. let selector = ''; @@ -93,11 +102,13 @@ export function extractDirectiveMetadata( } // Determine if `ngOnChanges` is a lifecycle hook defined on the component. - const usesOnChanges = reflectNonStaticField(clazz, 'ngOnChanges') !== null; + const usesOnChanges = members.find( + member => member.isStatic && member.kind === ClassMemberKind.Method && + member.name === 'ngOnChanges') !== undefined; return { name: clazz.name !.text, - deps: getConstructorDependencies(clazz, checker), + deps: getConstructorDependencies(clazz, reflector), host: { attributes: {}, listeners: {}, @@ -123,35 +134,6 @@ function assertIsStringArray(value: any[]): value is string[] { return true; } -type DecoratedProperty = DecoratedNode; - -/** - * Find all fields in the array of `DecoratedNode`s that have a decorator of the given type. - */ -function findDecoratedFields( - elements: DecoratedNode[], 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. @@ -185,14 +167,15 @@ function parseFieldToPropertyMapping( * object. */ function parseDecoratedFields( - fields: DecoratedProperty[], checker: ts.TypeChecker): {[field: string]: string} { + fields: {member: ClassMember, decorators: Decorator[]}[], + checker: ts.TypeChecker): {[field: string]: string} { return fields.reduce( (results, field) => { - const fieldName = (field.element.name as ts.Identifier).text; + const fieldName = field.member.name; 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) { + if (decorator.args == null || decorator.args.length === 0) { results[fieldName] = fieldName; } else if (decorator.args.length === 1) { const property = staticallyResolve(decorator.args[0], checker); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index f0210e78c1..e93baf233d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -9,26 +9,26 @@ import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; 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 {Decorator, ReflectionHost} from '../../host'; +import {reflectObjectLiteral} from '../../metadata'; +import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; -import {getConstructorDependencies} from './util'; +import {getConstructorDependencies, isAngularCore} from './util'; /** * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ export class InjectableDecoratorHandler implements DecoratorHandler { - constructor(private checker: ts.TypeChecker) {} + constructor(private reflector: ReflectionHost) {} detect(decorator: Decorator[]): Decorator|undefined { - return decorator.find(dec => dec.name === 'Injectable' && dec.from === '@angular/core'); + return decorator.find(decorator => decorator.name === 'Injectable' && isAngularCore(decorator)); } analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { return { - analysis: extractInjectableMetadata(node, decorator, this.checker), + analysis: extractInjectableMetadata(node, decorator, this.reflector), }; } @@ -49,18 +49,21 @@ export class InjectableDecoratorHandler implements DecoratorHandler 0) { throw new Error(`deps not yet supported`); } - deps.push(...depsExpr.elements.map(dep => getDep(dep, checker))); + deps.push(...depsExpr.elements.map(dep => getDep(dep, reflector))); } return {name, type, providedIn, useFactory: factory, deps}; } else { - const deps = getConstructorDependencies(clazz, checker); + const deps = getConstructorDependencies(clazz, reflector); return {name, type, providedIn, deps}; } } else { @@ -109,7 +112,7 @@ function extractInjectableMetadata( -function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata { +function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata { const meta: R3DependencyMetadata = { token: new WrappedNodeExpr(dep), host: false, @@ -119,8 +122,9 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetada skipSelf: false, }; - function maybeUpdateDecorator(dec: ts.Identifier, token?: ts.Expression): void { - const source = reflectImportedIdentifier(dec, checker); + function maybeUpdateDecorator( + dec: ts.Identifier, reflector: ReflectionHost, token?: ts.Expression): void { + const source = reflector.getImportOfIdentifier(dec); if (source === null || source.from !== '@angular/core') { return; } @@ -145,10 +149,10 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetada if (ts.isArrayLiteralExpression(dep)) { dep.elements.forEach(el => { if (ts.isIdentifier(el)) { - maybeUpdateDecorator(el); + maybeUpdateDecorator(el, reflector); } else if (ts.isNewExpression(el) && ts.isIdentifier(el.expression)) { const token = el.arguments && el.arguments.length > 0 && el.arguments[0] || undefined; - maybeUpdateDecorator(el.expression, token); + maybeUpdateDecorator(el.expression, reflector, token); } }); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index b39e2c561d..7ca9acb324 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -9,11 +9,12 @@ 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 {Decorator} from '../../host'; +import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {SelectorScopeRegistry} from './selector_scope'; -import {referenceToExpression} from './util'; +import {isAngularCore, referenceToExpression} from './util'; /** * Compiles @NgModule annotations to ngModuleDef fields. @@ -24,11 +25,13 @@ export class NgModuleDecoratorHandler implements DecoratorHandler decorator.name === 'NgModule' && decorator.from === '@angular/core'); + return decorators.find(decorator => decorator.name === 'NgModule' && isAngularCore(decorator)); } analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + if (decorator.args === null || decorator.args.length !== 1) { + throw new Error(`Incorrect number of arguments to @NgModule decorator`); + } const meta = decorator.args[0]; if (!ts.isObjectLiteralExpression(meta)) { throw new Error(`Decorator argument must be literal.`); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts index 4ddc245ad7..0c067fb5f3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts @@ -9,11 +9,14 @@ import {Expression, ExternalExpr, ExternalReference} from '@angular/compiler'; import * as ts from 'typescript'; -import {AbsoluteReference, Reference, reflectStaticField, reflectTypeEntityToDeclaration} from '../../metadata'; +import {ReflectionHost} from '../../host'; +import {AbsoluteReference, Reference, reflectTypeEntityToDeclaration} from '../../metadata'; +import {reflectIdentifierOfDeclaration, reflectNameOfDeclaration} from '../../metadata/src/reflector'; import {referenceToExpression} from './util'; + /** * Metadata extracted for a given NgModule that can be used to compute selector scopes. */ @@ -59,55 +62,55 @@ export class SelectorScopeRegistry { /** * Map of modules declared in the current compilation unit to their (local) metadata. */ - private _moduleToData = new Map(); + private _moduleToData = new Map(); /** * Map of modules to their cached `CompilationScope`s. */ - private _compilationScopeCache = new Map>(); + private _compilationScopeCache = new Map>(); /** * Map of components/directives to their selector. */ - private _directiveToSelector = new Map(); + private _directiveToSelector = new Map(); /** * Map of pipes to their name. */ - private _pipeToName = new Map(); + private _pipeToName = new Map(); /** * Map of components/directives/pipes to their module. */ - private _declararedTypeToModule = new Map(); + private _declararedTypeToModule = new Map(); - constructor(private checker: ts.TypeChecker) {} + constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} /** * Register a module's metadata with the registry. */ - registerModule(node: ts.ClassDeclaration, data: ModuleData): void { - node = ts.getOriginalNode(node) as ts.ClassDeclaration; + registerModule(node: ts.Declaration, data: ModuleData): void { + node = ts.getOriginalNode(node) as ts.Declaration; if (this._moduleToData.has(node)) { - throw new Error(`Module already registered: ${node.name!.text}`); + throw new Error(`Module already registered: ${reflectNameOfDeclaration(node)}`); } 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); + this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.Declaration, 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; + registerSelector(node: ts.Declaration, selector: string): void { + node = ts.getOriginalNode(node) as ts.Declaration; if (this._directiveToSelector.has(node)) { - throw new Error(`Selector already registered: ${node.name!.text} ${selector}`); + throw new Error(`Selector already registered: ${reflectNameOfDeclaration(node)} ${selector}`); } this._directiveToSelector.set(node, selector); } @@ -115,14 +118,14 @@ export class SelectorScopeRegistry { /** * Register the name of a pipe with the registry. */ - registerPipe(node: ts.ClassDeclaration, name: string): void { this._pipeToName.set(node, name); } + registerPipe(node: ts.Declaration, 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|null { - node = ts.getOriginalNode(node) as ts.ClassDeclaration; + lookupCompilationScope(node: ts.Declaration): CompilationScope|null { + node = ts.getOriginalNode(node) as ts.Declaration; // If the component has no associated module, then it has no compilation scope. if (!this._declararedTypeToModule.has(node)) { @@ -150,8 +153,7 @@ export class SelectorScopeRegistry { // 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); + const selector = this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.Declaration); // Only directives/components with selectors get added to the scope. if (selector != null) { directives.set(selector, ref); @@ -174,8 +176,7 @@ export class SelectorScopeRegistry { * (`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 { + private lookupScopes(node: ts.Declaration, ngModuleImportedFrom: string|null): SelectorScopes { let data: ModuleData|null = null; // Either this module was analyzed directly, or has a precompiled ngModuleDef. @@ -195,7 +196,7 @@ export class SelectorScopeRegistry { } if (data === null) { - throw new Error(`Module not registered: ${node.name!.text}`); + throw new Error(`Module not registered: ${reflectNameOfDeclaration(node)}`); } return { @@ -203,20 +204,18 @@ export class SelectorScopeRegistry { ...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)), + ref => + this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)).exported)), // And include the compilation scope of exported modules. ...flatten( - data.exports.filter(ref => this._moduleToData.has(ref.node as ts.ClassDeclaration)) + data.exports.filter(ref => this._moduleToData.has(ref.node as ts.Declaration)) .map( - ref => - this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref)) - .exported)) + ref => this.lookupScopes(ref.node as ts.Declaration, 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; + if (this._moduleToData.has(ref.node as ts.Declaration)) { + return this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)).exported; } else { return [ref]; } @@ -231,7 +230,7 @@ export class SelectorScopeRegistry { * ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read * to determine the selector. */ - private lookupDirectiveSelector(node: ts.ClassDeclaration): string|null { + private lookupDirectiveSelector(node: ts.Declaration): string|null { if (this._directiveToSelector.has(node)) { return this._directiveToSelector.get(node) !; } else { @@ -239,7 +238,7 @@ export class SelectorScopeRegistry { } } - private lookupPipeName(node: ts.ClassDeclaration): string|undefined { + private lookupPipeName(node: ts.Declaration): string|undefined { return this._pipeToName.get(node); } @@ -251,16 +250,17 @@ export class SelectorScopeRegistry { * @param ngModuleImportedFrom module specifier of the import path to assume for all declarations * stemming from this module. */ - private _readMetadataFromCompiledClass(clazz: ts.ClassDeclaration, ngModuleImportedFrom: string): + private _readMetadataFromCompiledClass(clazz: ts.Declaration, 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) { + const ngModuleDef = this.reflector.getMembersOfClass(clazz).find( + member => member.name === 'ngModuleDef' && member.isStatic); + if (ngModuleDef === undefined) { return null; } else if ( // Validate that the shape of the ngModuleDef type is correct. - ngModuleDef.type === undefined || !ts.isTypeReferenceNode(ngModuleDef.type) || + ngModuleDef.type === null || !ts.isTypeReferenceNode(ngModuleDef.type) || ngModuleDef.type.typeArguments === undefined || ngModuleDef.type.typeArguments.length !== 4) { return null; @@ -279,14 +279,15 @@ export class SelectorScopeRegistry { * 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) { + private _readSelectorFromCompiledClass(clazz: ts.Declaration): string|null { + const def = this.reflector.getMembersOfClass(clazz).find( + field => + field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef')); + if (def === undefined) { // No definition could be found. return null; } else if ( - def.type === undefined || !ts.isTypeReferenceNode(def.type) || + def.type === null || !ts.isTypeReferenceNode(def.type) || def.type.typeArguments === undefined || def.type.typeArguments.length !== 2) { // The type metadata was the wrong shape. return null; @@ -317,8 +318,9 @@ export class SelectorScopeRegistry { 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); + const clazz = node as ts.Declaration; + const id = reflectIdentifierOfDeclaration(clazz); + return new AbsoluteReference(node, id !, moduleName, id !.text); }); } } @@ -331,7 +333,6 @@ function flatten(array: T[][]): T[] { } function absoluteModuleName(ref: Reference): string|null { - const name = (ref.node as ts.ClassDeclaration).name !.text; if (!(ref instanceof AbsoluteReference)) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 0f3ee2442d..4a17e086ea 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -9,20 +9,20 @@ import {Expression, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; -import {Reference, reflectConstructorParameters} from '../../metadata'; -import {reflectImportedIdentifier} from '../../metadata/src/reflector'; +import {Decorator, ReflectionHost} from '../../host'; +import {Reference} from '../../metadata'; export function getConstructorDependencies( - clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] { + clazz: ts.ClassDeclaration, reflector: ReflectionHost): R3DependencyMetadata[] { const useType: R3DependencyMetadata[] = []; - const ctorParams = (reflectConstructorParameters(clazz, checker) || []); - ctorParams.forEach(param => { - let tokenExpr = param.typeValueExpr; + const ctorParams = reflector.getConstructorParameters(clazz) || []; + ctorParams.forEach((param, idx) => { + let tokenExpr = param.type; let optional = false, self = false, skipSelf = false, host = false; let resolved = R3ResolvedDependencyType.Token; - param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => { + (param.decorators || []).filter(isAngularCore).forEach(dec => { if (dec.name === 'Inject') { - if (dec.args.length !== 1) { + if (dec.args === null || dec.args.length !== 1) { throw new Error(`Unexpected number of arguments to @Inject().`); } tokenExpr = dec.args[0]; @@ -35,7 +35,7 @@ export function getConstructorDependencies( } else if (dec.name === 'Host') { host = true; } else if (dec.name === 'Attribute') { - if (dec.args.length !== 1) { + if (dec.args === null || dec.args.length !== 1) { throw new Error(`Unexpected number of arguments to @Attribute().`); } tokenExpr = dec.args[0]; @@ -46,10 +46,10 @@ export function getConstructorDependencies( }); 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(',')}`); + `No suitable token for parameter ${param.name || idx} of class ${clazz.name!.text}`); } if (ts.isIdentifier(tokenExpr)) { - const importedSymbol = reflectImportedIdentifier(tokenExpr, checker); + const importedSymbol = reflector.getImportOfIdentifier(tokenExpr); if (importedSymbol !== null && importedSymbol.from === '@angular/core') { switch (importedSymbol.name) { case 'ElementRef': @@ -82,3 +82,7 @@ export function referenceToExpression(ref: Reference, context: ts.SourceFile): E } return exp; } + +export function isAngularCore(decorator: Decorator): boolean { + return decorator.import !== null && decorator.import.from === '@angular/core'; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts index a5c28160ed..87ff276bd0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; +import {TypeScriptReflectionHost} from '../../metadata'; import {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {NgModuleDecoratorHandler} from '../src/ng_module'; @@ -53,6 +54,7 @@ describe('SelectorScopeRegistry', () => { }, ]); const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); const ProgramModule = getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); @@ -61,7 +63,7 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); - const registry = new SelectorScopeRegistry(checker); + const registry = new SelectorScopeRegistry(checker, host); registry.registerModule(ProgramModule, { declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], diff --git a/packages/compiler-cli/src/ngtsc/host/BUILD.bazel b/packages/compiler-cli/src/ngtsc/host/BUILD.bazel new file mode 100644 index 0000000000..ee3883cb08 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/host/BUILD.bazel @@ -0,0 +1,12 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "host", + srcs = glob([ + "index.ts", + "src/**/*.ts", + ]), + module_name = "@angular/compiler-cli/src/ngtsc/host", +) diff --git a/packages/compiler-cli/src/ngtsc/host/index.ts b/packages/compiler-cli/src/ngtsc/host/index.ts new file mode 100644 index 0000000000..93a06d7a93 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/host/index.ts @@ -0,0 +1,9 @@ +/** + * @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 * from './src/reflection'; diff --git a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts new file mode 100644 index 0000000000..c6c809737b --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts @@ -0,0 +1,223 @@ +/** + * @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'; + +/** + * Metadata extracted from an instance of a decorator on another declaration. + */ +export interface Decorator { + /** + * Name by which the decorator was invoked in the user's code. + * + * This is distinct from the name by which the decorator was imported (though in practice they + * will usually be the same). + */ + name: string; + + /** + * `Import` by which the decorator was brought into the module in which it was invoked, or `null` + * if the decorator was declared in the same module and not imported. + */ + import : Import | null; + + /** + * TypeScript reference to the decorator itself. + */ + node: ts.Node; + + /** + * Arguments of the invocation of the decorator, if the decorator is invoked, or `null` otherwise. + */ + args: ts.Expression[]|null; +} + +/** + * An enumeration of possible kinds of class members. + */ +export enum ClassMemberKind { + Constructor, + Getter, + Setter, + Property, + Method, +} + +/** + * A member of a class, such as a property, method, or constructor. + */ +export interface ClassMember { + /** + * TypeScript reference to the class member itself. + */ + node: ts.Node; + + /** + * Indication of which type of member this is (property, method, etc). + */ + kind: ClassMemberKind; + + /** + * TypeScript `ts.TypeNode` representing the type of the member, or `null` if not present or + * applicable. + */ + type: ts.TypeNode|null; + + /** + * Name of the class member. + */ + name: string; + + /** + * TypeScript `ts.Identifier` representing the name of the member, or `null` if no such node + * is present. + * + * The `nameNode` is useful in writing references to this member that will be correctly source- + * mapped back to the original file. + */ + nameNode: ts.Identifier|null; + + /** + * TypeScript `ts.Expression` which initializes this member, if the member is a property, or + * `null` otherwise. + */ + initializer: ts.Expression|null; + + /** + * Whether the member is static or not. + */ + isStatic: boolean; + + /** + * Any `Decorator`s which are present on the member, or `null` if none are present. + */ + decorators: Decorator[]|null; +} + +/** + * A parameter to a function or constructor. + */ +export interface Parameter { + /** + * Name of the parameter, if available. + * + * Some parameters don't have a simple string name (for example, parameters which are destructured + * into multiple variables). In these cases, `name` can be `null`. + */ + name: string|null; + + /** + * TypeScript `ts.BindingName` representing the name of the parameter. + * + * The `nameNode` is useful in writing references to this member that will be correctly source- + * mapped back to the original file. + */ + nameNode: ts.BindingName; + + /** + * TypeScript `ts.Expression` representing the type of the parameter, if the type is a simple + * expression type. + * + * If the type is not present or cannot be represented as an expression, `type` is `null`. + */ + type: ts.Expression|null; + + /** + * Any `Decorator`s which are present on the parameter, or `null` if none are present. + */ + decorators: Decorator[]|null; +} + +/** + * The source of an imported symbol, including the original symbol name and the module from which it + * was imported. + */ +export interface Import { + /** + * The name of the imported symbol under which it was exported (not imported). + */ + name: string; + + /** + * The module from which the symbol was imported. + * + * This could either be an absolute module name (@angular/core for example) or a relative path. + */ + from: string; +} + +/** + * Abstracts reflection operations on a TypeScript AST. + * + * Depending on the format of the code being interpreted, different concepts are represented with + * different syntactical structures. The `ReflectionHost` abstracts over those differences and + * presents a single API by which the compiler can query specific information about the AST. + * + * All operations on the `ReflectionHost` require the use of TypeScript `ts.Node`s with binding + * information already available (that is, nodes that come from a `ts.Program` that has been + * type-checked, and are not synthetically created). + */ +export interface ReflectionHost { + /** + * Examine a declaration (for example, of a class or function) and return metadata about any + * decorators present on the declaration. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class or function over + * which to reflect. For example, if the intent is to reflect the decorators of a class and the + * source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5 + * format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the + * result of an IIFE execution. + * + * @returns an array of `Decorator` metadata if decorators are present on the declaration, or + * `null` if either no decorators were present or if the declaration is not of a decorable type. + */ + getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null; + + /** + * Examine a declaration which should be of a class, and return metadata about the members of the + * class. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `ClassMember` metadata representing the members of the class. + * + * @throws if `declaration` does not resolve to a class declaration. + */ + getMembersOfClass(clazz: ts.Declaration): ClassMember[]; + + /** + * Reflect over the constructor of a class and return metadata about its parameters. + * + * This method only looks at the constructor of a class directly and not at any inherited + * constructors. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `Parameter` metadata representing the parameters of the constructor, if + * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. + * If the class has no constructor, this method returns `null`. + */ + getConstructorParameters(declaration: ts.Declaration): Parameter[]|null; + + /** + * Determine if an identifier was imported from another module and return `Import` metadata + * describing its origin. + * + * @param id a TypeScript `ts.Identifer` to reflect. + * + * @returns metadata about the `Import` if the identifier was imported from another module, or + * `null` if the identifier doesn't resolve to an import but instead is locally defined. + */ + getImportOfIdentifier(id: ts.Identifier): Import|null; +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel index 4a5c83e87c..6780665ad1 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel @@ -12,5 +12,6 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/host", ], ) diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index 49b2efca8d..dd4a581356 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -6,6 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator, reflectNonStaticField, reflectObjectLiteral, reflectStaticField, reflectTypeEntityToDeclaration,} from './src/reflector'; - +export {TypeScriptReflectionHost, filterToMembersWithDecorator, findMember, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/reflector'; export {AbsoluteReference, Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts index 4b807bbb3a..61cab4e721 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts @@ -8,289 +8,187 @@ import * as ts from 'typescript'; +import {ClassMember, ClassMemberKind, Decorator, Import, Parameter, ReflectionHost} from '../../host'; + /** * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. */ -/** - * A reflected parameter of a function, method, or constructor, indicating the name, any - * decorators, and an expression representing a reference to the value side of the parameter's - * declared type, if applicable. - */ -export interface Parameter { - /** - * Name of the parameter as a `ts.BindingName`, which allows the parameter name to be identified - * via sourcemaps. - */ - name: ts.BindingName; +export class TypeScriptReflectionHost implements ReflectionHost { + constructor(protected checker: ts.TypeChecker) {} - /** - * A `ts.Expression` which represents a reference to the value side of the parameter's type. - */ - typeValueExpr: ts.Expression|null; - - /** - * Array of decorators present on the parameter. - */ - decorators: Decorator[]; -} - -/** - * A reflected decorator, indicating the name, where it was imported from, and any arguments if the - * decorator is a call expression. - */ -export interface Decorator { - /** - * Name of the decorator, extracted from the decoration expression. - */ - name: string; - - /** - * Import path (relative to the decorator's file) of the decorator itself. - */ - from: string; - - /** - * The decorator node itself (useful for printing sourcemap based references to the decorator). - */ - node: ts.Decorator; - - /** - * Any arguments of a call expression, if one is present. If the decorator was not a call - * expression, then this will be an empty array. - */ - args: ts.Expression[]; -} - -/** - * Reflect a `ts.ClassDeclaration` and determine the list of parameters. - * - * Note that this only reflects the referenced class and not any potential parent class - that must - * be handled by the caller. - * - * @param node the `ts.ClassDeclaration` to reflect - * @param checker a `ts.TypeChecker` used for reflection - * @returns a `Parameter` instance for each argument of the constructor, or `null` if no constructor - */ -export function reflectConstructorParameters( - node: ts.ClassDeclaration, checker: ts.TypeChecker): Parameter[]|null { - // Firstly, look for a constructor. - // clang-format off - const maybeCtor: ts.ConstructorDeclaration[] = node - .members - .filter(element => ts.isConstructorDeclaration(element)) as ts.ConstructorDeclaration[]; - // clang-format on - - if (maybeCtor.length !== 1) { - // No constructor. - return null; - } - - // Reflect each parameter. - return maybeCtor[0].parameters.map(param => reflectParameter(param, checker)); -} - -/** - * Reflect a `ts.ParameterDeclaration` and determine its name, a token which refers to the value - * declaration of its type (if possible to statically determine), and its decorators, if any. - */ -function reflectParameter(node: ts.ParameterDeclaration, checker: ts.TypeChecker): Parameter { - // The name of the parameter is easy. - const name = node.name; - - const decorators = node.decorators && - node.decorators.map(decorator => reflectDecorator(decorator, checker)) - .filter(decorator => decorator !== null) as Decorator[] || - []; - - // It may or may not be possible to write an expression that refers to the value side of the - // type named for the parameter. - let typeValueExpr: ts.Expression|null = null; - - // It's not possible to get a value expression if the parameter doesn't even have a type. - if (node.type !== undefined) { - // It's only valid to convert a type reference to a value reference if the type actually has a - // value declaration associated with it. - const type = checker.getTypeFromTypeNode(node.type); - if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) { - // The type points to a valid value declaration. Rewrite the TypeReference into an Expression - // which references the value pointed to by the TypeReference, if possible. - typeValueExpr = typeNodeToValueExpr(node.type); + getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null { + if (declaration.decorators === undefined || declaration.decorators.length === 0) { + return null; } + return declaration.decorators.map(decorator => this._reflectDecorator(decorator)) + .filter((dec): dec is Decorator => dec !== null); } - return { - name, typeValueExpr, decorators, - }; -} - -/** - * Reflect a decorator and return a structure describing where it comes from and any arguments. - * - * Only imported decorators are considered, not locally defined decorators. - */ -export function reflectDecorator(decorator: ts.Decorator, checker: ts.TypeChecker): Decorator|null { - // Attempt to resolve the decorator expression into a reference to a concrete Identifier. The - // expression may contain a call to a function which returns the decorator function, in which - // case we want to return the arguments. - let decoratorOfInterest: ts.Expression = decorator.expression; - let args: ts.Expression[] = []; - - // Check for call expressions. - if (ts.isCallExpression(decoratorOfInterest)) { - args = Array.from(decoratorOfInterest.arguments); - decoratorOfInterest = decoratorOfInterest.expression; + getMembersOfClass(declaration: ts.Declaration): ClassMember[] { + const clazz = castDeclarationToClassOrDie(declaration); + return clazz.members.map(member => this._reflectMember(member)) + .filter((member): member is ClassMember => member !== null); } - // The final resolved decorator should be a `ts.Identifier` - if it's not, then something is - // wrong and the decorator can't be resolved statically. - if (!ts.isIdentifier(decoratorOfInterest)) { - return null; - } + getConstructorParameters(declaration: ts.Declaration): Parameter[]|null { + const clazz = castDeclarationToClassOrDie(declaration); - const importDecl = reflectImportedIdentifier(decoratorOfInterest, checker); - if (importDecl === null) { - return null; - } + // First, find the constructor. + const ctor = clazz.members.find(ts.isConstructorDeclaration); + if (ctor === undefined) { + return null; + } - return { - ...importDecl, - node: decorator, args, - }; -} + return ctor.parameters.map(node => { + // The name of the parameter is easy. + const name = parameterName(node.name); -function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null { - if (ts.isTypeReferenceNode(node)) { - return entityNameToValue(node.typeName); - } else { - return null; - } -} + const decorators = this.getDecoratorsOfDeclaration(node); -function entityNameToValue(node: ts.EntityName): ts.Expression|null { - if (ts.isQualifiedName(node)) { - const left = entityNameToValue(node.left); - return left !== null ? ts.createPropertyAccess(left, node.right) : null; - } else if (ts.isIdentifier(node)) { - return ts.getMutableClone(node); - } else { - return null; - } -} + // It may or may not be possible to write an expression that refers to the value side of the + // type named for the parameter. + let typeValueExpr: ts.Expression|null = null; -function propertyNameToValue(node: ts.PropertyName): string|null { - if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { - return node.text; - } else { - return null; - } -} - -export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map { - const map = new Map(); - node.properties.forEach(prop => { - if (ts.isPropertyAssignment(prop)) { - const name = propertyNameToValue(prop.name); - if (name === null) { - return; + // It's not possible to get a value expression if the parameter doesn't even have a type. + if (node.type !== undefined) { + // It's only valid to convert a type reference to a value reference if the type actually has + // a + // value declaration associated with it. + const type = this.checker.getTypeFromTypeNode(node.type); + if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) { + // The type points to a valid value declaration. Rewrite the TypeReference into an + // Expression + // which references the value pointed to by the TypeReference, if possible. + typeValueExpr = typeNodeToValueExpr(node.type); + } } - map.set(name, prop.initializer); - } else if (ts.isShorthandPropertyAssignment(prop)) { - map.set(prop.name.text, prop.name); + + return { + name, + nameNode: node.name, + type: typeValueExpr, decorators, + }; + }); + } + + getImportOfIdentifier(id: ts.Identifier): Import|null { + const symbol = this.checker.getSymbolAtLocation(id); + + if (symbol === undefined || symbol.declarations === undefined || + symbol.declarations.length !== 1) { + return null; + } + + // Ignore decorators that are defined locally (not imported). + const decl: ts.Declaration = symbol.declarations[0]; + if (!ts.isImportSpecifier(decl)) { + return null; + } + + // Walk back from the specifier to find the declaration, which carries the module specifier. + const importDecl = decl.parent !.parent !.parent !; + + // The module specifier is guaranteed to be a string literal, so this should always pass. + if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { + // Not allowed to happen in TypeScript ASTs. + return null; + } + + // Read the module specifier. + const from = importDecl.moduleSpecifier.text; + + // Compute the name by which the decorator was exported, not imported. + const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text; + + return {from, name}; + } + + isClass(node: ts.Node): node is ts.Declaration { return ts.isClassDeclaration(node); } + + private _reflectDecorator(node: ts.Decorator): Decorator|null { + // Attempt to resolve the decorator expression into a reference to a concrete Identifier. The + // expression may contain a call to a function which returns the decorator function, in which + // case we want to return the arguments. + let decoratorExpr: ts.Expression = node.expression; + let args: ts.Expression[]|null = null; + + // Check for call expressions. + if (ts.isCallExpression(decoratorExpr)) { + args = Array.from(decoratorExpr.arguments); + decoratorExpr = decoratorExpr.expression; + } + + // The final resolved decorator should be a `ts.Identifier` - if it's not, then something is + // wrong and the decorator can't be resolved statically. + if (!ts.isIdentifier(decoratorExpr)) { + return null; + } + + const importDecl = this.getImportOfIdentifier(decoratorExpr); + + return { + name: decoratorExpr.text, + import: importDecl, node, args, + }; + } + + private _reflectMember(node: ts.ClassElement): ClassMember|null { + let kind: ClassMemberKind|null = null; + let initializer: ts.Expression|null = null; + let name: string|null = null; + let nameNode: ts.Identifier|null = null; + + if (ts.isPropertyDeclaration(node)) { + kind = ClassMemberKind.Property; + initializer = node.initializer || null; + } else if (ts.isGetAccessorDeclaration(node)) { + kind = ClassMemberKind.Getter; + } else if (ts.isSetAccessorDeclaration(node)) { + kind = ClassMemberKind.Setter; + } else if (ts.isMethodDeclaration(node)) { + kind = ClassMemberKind.Method; + } else if (ts.isConstructorDeclaration(node)) { + kind = ClassMemberKind.Constructor; } else { - return; + return null; } - }); - return map; -} -export function reflectImportedIdentifier( - id: ts.Identifier, checker: ts.TypeChecker): {name: string, from: string}|null { - const symbol = checker.getSymbolAtLocation(id); + if (ts.isConstructorDeclaration(node)) { + name = 'constructor'; + } else if (ts.isIdentifier(node.name)) { + name = node.name.text; + nameNode = node.name; + } else { + return null; + } - if (symbol === undefined || symbol.declarations === undefined || - symbol.declarations.length !== 1) { - return null; + const decorators = this.getDecoratorsOfDeclaration(node); + const isStatic = node.modifiers !== undefined && + node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + + return { + node, + kind, + type: node.type || null, name, nameNode, decorators, initializer, isStatic, + }; } +} - // Ignore decorators that are defined locally (not imported). - const decl: ts.Declaration = symbol.declarations[0]; - if (!ts.isImportSpecifier(decl)) { - return null; +export function reflectNameOfDeclaration(decl: ts.Declaration): string|null { + const id = reflectIdentifierOfDeclaration(decl); + return id && id.text || null; +} + +export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier|null { + if (ts.isClassDeclaration(decl) || ts.isFunctionDeclaration(decl)) { + return decl.name || null; + } else if (ts.isVariableDeclaration(decl)) { + if (ts.isIdentifier(decl.name)) { + return decl.name; + } } - - // Walk back from the specifier to find the declaration, which carries the module specifier. - const importDecl = decl.parent !.parent !.parent !; - - // The module specifier is guaranteed to be a string literal, so this should always pass. - if (!ts.isStringLiteral(importDecl.moduleSpecifier)) { - // Not allowed to happen in TypeScript ASTs. - return null; - } - - // Read the module specifier. - const from = importDecl.moduleSpecifier.text; - - // Compute the name by which the decorator was exported, not imported. - const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text; - - return {from, name}; -} - -export interface DecoratedNode { - element: T; - decorators: Decorator[]; -} - -export function getDecoratedClassElements( - clazz: ts.ClassDeclaration, checker: ts.TypeChecker): DecoratedNode[] { - const decoratedElements: DecoratedNode[] = []; - 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; + return null; } export function reflectTypeEntityToDeclaration( @@ -335,4 +233,95 @@ export function reflectTypeEntityToDeclaration( } else { return {node, from: null}; } -} \ No newline at end of file +} + +export function filterToMembersWithDecorator(members: ClassMember[], name: string, module?: string): + {member: ClassMember, decorators: Decorator[]}[] { + return members.filter(member => !member.isStatic) + .map(member => { + if (member.decorators === null) { + return null; + } + + const decorators = member.decorators.filter(dec => { + if (dec.import !== null) { + return dec.import.name === name && (module === undefined || dec.import.from === module); + } else { + return dec.name === name && module === undefined; + } + }); + + if (decorators.length === 0) { + return null; + } + + return {member, decorators}; + }) + .filter((value): value is {member: ClassMember, decorators: Decorator[]} => value !== null); +} + +export function findMember( + members: ClassMember[], name: string, isStatic: boolean = false): ClassMember|null { + return members.find(member => member.isStatic === isStatic && member.name === name) || null; +} + +export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map { + const map = new Map(); + node.properties.forEach(prop => { + if (ts.isPropertyAssignment(prop)) { + const name = propertyNameToString(prop.name); + if (name === null) { + return; + } + map.set(name, prop.initializer); + } else if (ts.isShorthandPropertyAssignment(prop)) { + map.set(prop.name.text, prop.name); + } else { + return; + } + }); + return map; +} + +function castDeclarationToClassOrDie(declaration: ts.Declaration): ts.ClassDeclaration { + if (!ts.isClassDeclaration(declaration)) { + throw new Error( + `Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`); + } + return declaration; +} + +function parameterName(name: ts.BindingName): string|null { + if (ts.isIdentifier(name)) { + return name.text; + } else { + return null; + } +} + +function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null { + if (ts.isTypeReferenceNode(node)) { + return entityNameToValue(node.typeName); + } else { + return null; + } +} + +function entityNameToValue(node: ts.EntityName): ts.Expression|null { + if (ts.isQualifiedName(node)) { + const left = entityNameToValue(node.left); + return left !== null ? ts.createPropertyAccess(left, node.right) : null; + } else if (ts.isIdentifier(node)) { + return ts.getMutableClone(node); + } else { + return null; + } +} + +function propertyNameToString(node: ts.PropertyName): string|null { + if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { + return node.text; + } else { + return null; + } +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel index 9a675329a2..65474c8342 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/testing", ], diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts index f7cf26bd17..245dc8937b 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts @@ -8,8 +8,9 @@ import * as ts from 'typescript'; +import {Parameter} from '../../host'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; -import {Parameter, reflectConstructorParameters} from '../src/reflector'; +import {TypeScriptReflectionHost} from '../src/reflector'; describe('reflector', () => { describe('ctor params', () => { @@ -26,9 +27,10 @@ describe('reflector', () => { }]); const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration); const checker = program.getTypeChecker(); - const args = reflectConstructorParameters(clazz, checker) !; + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(1); - expectArgument(args[0], 'bar', 'Bar'); + expectParameter(args[0], 'bar', 'Bar'); }); it('should reflect a decorated argument', () => { @@ -54,9 +56,10 @@ describe('reflector', () => { ]); const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration); const checker = program.getTypeChecker(); - const args = reflectConstructorParameters(clazz, checker) !; + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(1); - expectArgument(args[0], 'bar', 'Bar', 'dec', './dec'); + expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); }); it('should reflect a decorated argument with a call', () => { @@ -82,9 +85,10 @@ describe('reflector', () => { ]); const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration); const checker = program.getTypeChecker(); - const args = reflectConstructorParameters(clazz, checker) !; + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(1); - expectArgument(args[0], 'bar', 'Bar', 'dec', './dec'); + expectParameter(args[0], 'bar', 'Bar', 'dec', './dec'); }); it('should reflect a decorated argument with an indirection', () => { @@ -109,26 +113,31 @@ describe('reflector', () => { ]); const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration); const checker = program.getTypeChecker(); - const args = reflectConstructorParameters(clazz, checker) !; + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(2); - expectArgument(args[0], 'bar', 'Bar'); - expectArgument(args[1], 'otherBar', 'star.Bar'); + expectParameter(args[0], 'bar', 'Bar'); + expectParameter(args[1], 'otherBar', 'star.Bar'); }); }); }); -function expectArgument( - arg: Parameter, name: string, type?: string, decorator?: string, decoratorFrom?: string): void { - expect(argExpressionToString(arg.name)).toEqual(name); +function expectParameter( + param: Parameter, name: string, type?: string, decorator?: string, + decoratorFrom?: string): void { + expect(param.name !).toEqual(name); if (type === undefined) { - expect(arg.typeValueExpr).toBeNull(); + expect(param.type).toBeNull(); } else { - expect(arg.typeValueExpr).not.toBeNull(); - expect(argExpressionToString(arg.typeValueExpr !)).toEqual(type); + expect(param.type).not.toBeNull(); + expect(argExpressionToString(param.type !)).toEqual(type); } if (decorator !== undefined) { - expect(arg.decorators.length).toBeGreaterThan(0); - expect(arg.decorators.some(dec => dec.name === decorator && dec.from === decoratorFrom)) + expect(param.decorators).not.toBeNull(); + expect(param.decorators !.length).toBeGreaterThan(0); + expect(param.decorators !.some( + dec => dec.name === decorator && dec.import !== null && + dec.import.from === decoratorFrom)) .toBe(true); } } diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 0df33b4627..95ef841abe 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -14,6 +14,7 @@ import * as api from '../transformers/api'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, SelectorScopeRegistry} from './annotations'; import {CompilerHost} from './compiler_host'; +import {TypeScriptReflectionHost} from './metadata'; import {IvyCompilation, ivyTransformFactory} from './transform'; export class NgtscProgram implements api.Program { @@ -90,16 +91,17 @@ export class NgtscProgram implements api.Program { const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults; const checker = this.tsProgram.getTypeChecker(); - const scopeRegistry = new SelectorScopeRegistry(checker); + const reflector = new TypeScriptReflectionHost(checker); + const scopeRegistry = new SelectorScopeRegistry(checker, reflector); // Set up the IvyCompilation, which manages state for the Ivy transformer. const handlers = [ - new ComponentDecoratorHandler(checker, scopeRegistry), - new DirectiveDecoratorHandler(checker, scopeRegistry), - new InjectableDecoratorHandler(checker), + new ComponentDecoratorHandler(checker, reflector, scopeRegistry), + new DirectiveDecoratorHandler(checker, reflector, scopeRegistry), + new InjectableDecoratorHandler(reflector), new NgModuleDecoratorHandler(checker, scopeRegistry), ]; - const compilation = new IvyCompilation(handlers, checker); + const compilation = new IvyCompilation(handlers, checker, reflector); // Analyze every source file in the program. this.tsProgram.getSourceFiles() diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index ab5918688c..a23b780019 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( module_name = "@angular/compiler-cli/src/ngtsc/transform", deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/util", ], diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 4ce4734a9d..6dac390d0b 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -9,7 +9,7 @@ import {Expression, Statement, Type} from '@angular/compiler'; import * as ts from 'typescript'; -import {Decorator} from '../../metadata'; +import {Decorator} from '../../host'; /** * Provides the interface between a decorator compiler from @angular/compiler and the Typescript @@ -31,13 +31,13 @@ export interface DecoratorHandler { * if successful, or an array of diagnostic messages if the analysis fails or the decorator * isn't valid. */ - analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput; + analyze(node: ts.Declaration, decorator: Decorator): AnalysisOutput; /** * 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): CompileResult; + compile(node: ts.Declaration, analysis: A): CompileResult; } /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 948a01ba6f..3500683998 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -9,7 +9,8 @@ import {Expression, Type} from '@angular/compiler'; import * as ts from 'typescript'; -import {Decorator, reflectDecorator} from '../../metadata'; +import {Decorator, ReflectionHost} from '../../host'; +import {reflectNameOfDeclaration} from '../../metadata/src/reflector'; import {AnalysisOutput, CompileResult, DecoratorHandler} from './api'; import {DtsFileTransformer} from './declaration'; @@ -24,7 +25,7 @@ import {ImportManager, translateType} from './translator'; interface EmitFieldOperation { adapter: DecoratorHandler; analysis: AnalysisOutput; - decorator: ts.Decorator; + decorator: Decorator; } /** @@ -38,59 +39,63 @@ export class IvyCompilation { * Tracks classes which have been analyzed and found to have an Ivy decorator, and the * information recorded about them for later compilation. */ - private analysis = new Map>(); + private analysis = new Map>(); /** * Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations. */ private dtsMap = new Map(); - constructor(private handlers: DecoratorHandler[], private checker: ts.TypeChecker) {} + constructor( + private handlers: DecoratorHandler[], private checker: ts.TypeChecker, + private reflector: ReflectionHost) {} /** * Analyze a source file and produce diagnostics for it (if any). */ analyze(sf: ts.SourceFile): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; - const visit = (node: ts.Node) => { - // Process nodes recursively, and look for class declarations with decorators. - if (ts.isClassDeclaration(node) && node.decorators !== undefined) { - // The first step is to reflect the decorators, which will identify decorators - // that are imported from another module. - const decorators = - node.decorators.map(decorator => reflectDecorator(decorator, this.checker)) - .filter(decorator => decorator !== null) as Decorator[]; - // 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) { - return; - } - - // Check for multiple decorators on the same node. Technically speaking this - // could be supported, but right now it's an error. - if (this.analysis.has(node)) { - throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); - } - - // Run analysis on the decorator. This will produce either diagnostics, an - // analysis result, or both. - const analysis = adapter.analyze(node, decorator); - if (analysis.diagnostics !== undefined) { - diagnostics.push(...analysis.diagnostics); - } - if (analysis.analysis !== undefined) { - this.analysis.set(node, { - adapter, - analysis: analysis.analysis, - decorator: decorator.node, - }); - } - }); + const analyzeClass = (node: ts.Declaration): void => { + // The first step is to reflect the decorators. + const decorators = this.reflector.getDecoratorsOfDeclaration(node); + if (decorators === null) { + return; } + // 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) { + return; + } + // Check for multiple decorators on the same node. Technically speaking this + // could be supported, but right now it's an error. + if (this.analysis.has(node)) { + throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); + } + + // Run analysis on the decorator. This will produce either diagnostics, an + // analysis result, or both. + const analysis = adapter.analyze(node, decorator); + if (analysis.diagnostics !== undefined) { + diagnostics.push(...analysis.diagnostics); + } + if (analysis.analysis !== undefined) { + this.analysis.set(node, { + adapter, + analysis: analysis.analysis, decorator, + }); + } + }); + }; + + const visit = (node: ts.Node): void => { + // Process nodes recursively, and look for class declarations with decorators. + if (ts.isClassDeclaration(node)) { + analyzeClass(node); + } ts.forEachChild(node, visit); }; @@ -102,9 +107,9 @@ 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): CompileResult|undefined { + compileIvyFieldFor(node: ts.Declaration): 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; + const original = ts.getOriginalNode(node) as ts.Declaration; if (!this.analysis.has(original)) { return undefined; } @@ -117,7 +122,7 @@ export class IvyCompilation { // which will allow the .d.ts to be transformed later. const fileName = node.getSourceFile().fileName; const dtsTransformer = this.getDtsTransformer(fileName); - dtsTransformer.recordStaticField(node.name !.text, res); + dtsTransformer.recordStaticField(reflectNameOfDeclaration(node) !, res); // Return the instruction to the transformer so the field will be added. return res; @@ -126,8 +131,8 @@ export class IvyCompilation { /** * Lookup the `ts.Decorator` which triggered transformation of a particular class declaration. */ - ivyDecoratorFor(node: ts.ClassDeclaration): ts.Decorator|undefined { - const original = ts.getOriginalNode(node) as ts.ClassDeclaration; + ivyDecoratorFor(node: ts.Declaration): Decorator|undefined { + const original = ts.getOriginalNode(node) as ts.Declaration; if (!this.analysis.has(original)) { return undefined; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index e13a887e65..6878b2278c 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -46,7 +46,8 @@ class IvyVisitor extends Visitor { node = ts.updateClassDeclaration( node, // Remove the decorator which triggered this compilation, leaving the others alone. - maybeFilterDecorator(node.decorators, this.compilation.ivyDecoratorFor(node) !), + maybeFilterDecorator( + node.decorators, this.compilation.ivyDecoratorFor(node) !.node as ts.Decorator), node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], [...node.members, property]); const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager)); diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index c2cb6551ac..b6b8f76ae9 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -488,4 +488,4 @@ export function parseHostBindings(host: {[key: string]: string}): { }); return {attributes, listeners, properties, animations}; -} \ No newline at end of file +}