diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index c0dfa93d1b..b99eff1bb8 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -895,7 +895,13 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N paramInfo[index] : {decorators: null, typeExpression: null}; const nameNode = node.name; - return {name: getNameText(nameNode), nameNode, typeExpression, typeNode: null, decorators}; + return { + name: getNameText(nameNode), + nameNode, + typeValueReference: + typeExpression !== null ? {local: true as true, expression: typeExpression} : null, + typeNode: null, decorators + }; }); } diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts index c3d09c225c..ee1bcba5ea 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts @@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; +import {expectTypeValueReferencesForParameters} from './util'; + const FILES = [ { name: '/some_directive.js', @@ -262,8 +264,10 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { expect(parameters !.map(parameter => parameter.name)).toEqual([ '_viewContainer', '_template', 'injected' ]); - expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([ - 'ViewContainerRef', 'TemplateRef', 'String' + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + 'String', ]); }); @@ -296,7 +300,10 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier + }).expression; const expectedDeclarationNode = getDeclaration( program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index e46f184450..e595ec0582 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +import {expectTypeValueReferencesForParameters} from './util'; + const SOME_DIRECTIVE_FILE = { name: '/some_directive.js', contents: ` @@ -930,9 +932,7 @@ describe('Fesm2015ReflectionHost', () => { expect(parameters.map(parameter => parameter.name)).toEqual([ '_viewContainer', '_template', 'injected' ]); - expect(parameters.map(parameter => parameter.typeExpression !.getText())).toEqual([ - 'ViewContainerRef', 'TemplateRef', 'undefined' - ]); + expectTypeValueReferencesForParameters(parameters, ['ViewContainerRef', 'TemplateRef', null]); }); it('should throw if the symbol is not a class', () => { @@ -1293,7 +1293,10 @@ describe('Fesm2015ReflectionHost', () => { const classNode = getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier + }).expression; const expectedDeclarationNode = getDeclaration( program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts index 55647fbb4f..6c558b1e48 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -12,6 +12,8 @@ import {ClassMemberKind, Import} from '../../../ngtsc/reflection'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils'; +import {expectTypeValueReferencesForParameters} from './util'; + const FILES = [ { name: '/some_directive.js', @@ -277,8 +279,10 @@ describe('Esm5ReflectionHost [import helper style]', () => { expect(parameters !.map(parameter => parameter.name)).toEqual([ '_viewContainer', '_template', 'injected' ]); - expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([ - 'ViewContainerRef', 'TemplateRef', 'String' + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + 'String', ]); }); @@ -332,7 +336,10 @@ describe('Esm5ReflectionHost [import helper style]', () => { const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier + }).expression; const expectedDeclarationNode = getDeclaration( program, '/some_directive.js', 'ViewContainerRef', ts.isVariableDeclaration); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 8efc087d2a..b83110b36a 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -13,6 +13,8 @@ import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {getDeclaration, makeTestProgram} from '../helpers/utils'; +import {expectTypeValueReferencesForParameters} from './util'; + const SOME_DIRECTIVE_FILE = { name: '/some_directive.js', contents: ` @@ -918,8 +920,10 @@ describe('Esm5ReflectionHost', () => { expect(parameters !.map(parameter => parameter.name)).toEqual([ '_viewContainer', '_template', 'injected' ]); - expect(parameters !.map(parameter => parameter.typeExpression !.getText())).toEqual([ - 'ViewContainerRef', 'TemplateRef', 'undefined' + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, ]); }); @@ -1251,7 +1255,10 @@ describe('Esm5ReflectionHost', () => { const classNode = getDeclaration( program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); const ctrDecorators = host.getConstructorParameters(classNode) !; - const identifierOfViewContainerRef = ctrDecorators[0].typeExpression !as ts.Identifier; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier + }).expression; const expectedDeclarationNode = getDeclaration( program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); diff --git a/packages/compiler-cli/src/ngcc/test/host/util.ts b/packages/compiler-cli/src/ngcc/test/host/util.ts new file mode 100644 index 0000000000..008eef7b45 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/util.ts @@ -0,0 +1,31 @@ + +/** + * @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 {CtorParameter} from '../../../ngtsc/reflection'; + +/** + * Check that a given list of `CtorParameter`s has `typeValueReference`s of specific `ts.Identifier` + * names. + */ +export function expectTypeValueReferencesForParameters( + parameters: CtorParameter[], expectedParams: (string | null)[]) { + parameters !.forEach((param, idx) => { + const expected = expectedParams[idx]; + if (expected !== null) { + if (param.typeValueReference === null || !param.typeValueReference.local || + !ts.isIdentifier(param.typeValueReference.expression)) { + fail(`Incorrect typeValueReference generated, expected ${expected}`); + } else { + expect(param.typeValueReference.expression.text).toEqual(expected); + } + } + }); +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts index dc8faa1673..c807e9a14d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/metadata.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ExternalExpr, Identifiers, InvokeFunctionExpr, Statement, WrappedNodeExpr} from '@angular/compiler'; +import {Expression, ExternalExpr, FunctionExpr, Identifiers, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, ReturnStatement, Statement, WrappedNodeExpr, literalMap} from '@angular/compiler'; import * as ts from 'typescript'; import {CtorParameter, Decorator, ReflectionHost} from '../../reflection'; +import {valueReferenceToExpression} from './util'; + /** * Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata * present on the class or its member fields. @@ -39,18 +41,13 @@ export function generateSetClassMetadataCall( const metaDecorators = ts.createArrayLiteral(ngClassDecorators); // Convert the constructor parameters to metadata, passing null if none are present. - let metaCtorParameters: ts.Expression = ts.createNull(); + let metaCtorParameters: Expression = new LiteralExpr(null); const classCtorParameters = reflection.getConstructorParameters(clazz); if (classCtorParameters !== null) { - const ctorParameters = ts.createArrayLiteral( - classCtorParameters.map(param => ctorParameterToMetadata(param, isCore))); - metaCtorParameters = ts.createFunctionExpression( - /* modifiers */ undefined, - /* asteriskToken */ undefined, - /* name */ undefined, - /* typeParameters */ undefined, - /* parameters */ undefined, - /* type */ undefined, ts.createBlock([ts.createReturn(ctorParameters)])); + const ctorParameters = classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)); + metaCtorParameters = new FunctionExpr([], [ + new ReturnStatement(new LiteralArrayExpr(ctorParameters)), + ]); } // Do the same for property decorators. @@ -71,7 +68,7 @@ export function generateSetClassMetadataCall( [ new WrappedNodeExpr(id), new WrappedNodeExpr(metaDecorators), - new WrappedNodeExpr(metaCtorParameters), + metaCtorParameters, new WrappedNodeExpr(metaPropDecorators), ], /* type */ undefined, @@ -83,22 +80,25 @@ export function generateSetClassMetadataCall( /** * Convert a reflected constructor parameter to metadata. */ -function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression { +function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Expression { // Parameters sometimes have a type that can be referenced. If so, then use it, otherwise // its type is undefined. - const type = - param.typeExpression !== null ? param.typeExpression : ts.createIdentifier('undefined'); - const properties: ts.ObjectLiteralElementLike[] = [ - ts.createPropertyAssignment('type', type), + const type = param.typeValueReference !== null ? + valueReferenceToExpression(param.typeValueReference) : + new LiteralExpr(undefined); + + const mapEntries: {key: string, value: Expression, quoted: false}[] = [ + {key: 'type', value: type, quoted: false}, ]; // If the parameter has decorators, include the ones from Angular. if (param.decorators !== null) { const ngDecorators = param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata); - properties.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(ngDecorators))); + const value = new WrappedNodeExpr(ts.createArrayLiteral(ngDecorators)); + mapEntries.push({key: 'decorators', value, quoted: false}); } - return ts.createObjectLiteral(properties, true); + return literalMap(mapEntries); } /** diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index a10ff41b54..65feb61606 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; +import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ImportMode, Reference, ReferenceEmitter} from '../../imports'; import {ForeignFunctionResolver} from '../../partial_evaluator'; -import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection'; +import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost, TypeValueReference} from '../../reflection'; export enum ConstructorDepErrorKind { NO_SUITABLE_TOKEN, @@ -45,7 +45,7 @@ export function getConstructorDependencies( } } ctorParams.forEach((param, idx) => { - let tokenExpr = param.typeExpression; + let token = valueReferenceToExpression(param.typeValueReference); let optional = false, self = false, skipSelf = false, host = false; let resolved = R3ResolvedDependencyType.Token; (param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => { @@ -56,7 +56,7 @@ export function getConstructorDependencies( ErrorCode.DECORATOR_ARITY_WRONG, dec.node, `Unexpected number of arguments to @Inject().`); } - tokenExpr = dec.args[0]; + token = new WrappedNodeExpr(dec.args[0]); } else if (name === 'Optional') { optional = true; } else if (name === 'SkipSelf') { @@ -71,20 +71,19 @@ export function getConstructorDependencies( ErrorCode.DECORATOR_ARITY_WRONG, dec.node, `Unexpected number of arguments to @Attribute().`); } - tokenExpr = dec.args[0]; + token = new WrappedNodeExpr(dec.args[0]); resolved = R3ResolvedDependencyType.Attribute; } else { throw new FatalDiagnosticError( ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`); } }); - if (tokenExpr === null) { + if (token === null) { errors.push({ index: idx, kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param, }); } else { - const token = new WrappedNodeExpr(tokenExpr); deps.push({token, optional, self, skipSelf, host, resolved}); } }); @@ -95,6 +94,27 @@ export function getConstructorDependencies( } } +/** + * Convert a `TypeValueReference` to an `Expression` which refers to the type as a value. + * + * Local references are converted to a `WrappedNodeExpr` of the TypeScript expression, and non-local + * references are converted to an `ExternalExpr`. Note that this is only valid in the context of the + * file in which the `TypeValueReference` originated. + */ +export function valueReferenceToExpression(valueRef: TypeValueReference): Expression; +export function valueReferenceToExpression(valueRef: null): null; +export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null; +export function valueReferenceToExpression(valueRef: TypeValueReference | null): Expression|null { + if (valueRef === null) { + return null; + } else if (valueRef.local) { + return new WrappedNodeExpr(valueRef.expression); + } else { + // TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here. + return new ExternalExpr(valueRef as{moduleName: string, name: string}); + } +} + export function getValidConstructorDependencies( clazz: ts.ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]| null { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts index adbbed46e5..837c685fcb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/metadata_spec.ts @@ -46,7 +46,7 @@ describe('ngtsc setClassMetadata converter', () => { } `); expect(res).toContain( - `function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }]; }, null);`); + `function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }]; }, null);`); }); it('should convert decorated field metadata', () => { diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index c8b5512694..ef46b8fd0d 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -151,6 +151,31 @@ export interface ClassMember { decorators: Decorator[]|null; } +/** + * A reference to a value that originated from a type position. + * + * For example, a constructor parameter could be declared as `foo: Foo`. A `TypeValueReference` + * extracted from this would refer to the value of the class `Foo` (assuming it was actually a + * type). + * + * There are two kinds of such references. A reference with `local: false` refers to a type that was + * imported, and gives the symbol `name` and the `moduleName` of the import. Note that this + * `moduleName` may be a relative path, and thus is likely only valid within the context of the file + * which contained the original type reference. + * + * A reference with `local: true` refers to any other kind of type via a `ts.Expression` that's + * valid within the local file where the type was referenced. + */ +export type TypeValueReference = { + local: true; expression: ts.Expression; +} | +{ + local: false; + name: string; + moduleName: string; + valueDeclaration: ts.Declaration; +}; + /** * A parameter to a constructor. */ @@ -172,18 +197,22 @@ export interface CtorParameter { nameNode: ts.BindingName; /** - * TypeScript `ts.Expression` representing the type value of the parameter, if the type is a - * simple - * expression type that can be converted to a value. + * Reference to the value of the parameter's type annotation, if it's possible to refer to the + * parameter's type as a value. * - * If the type is not present or cannot be represented as an expression, `type` is `null`. + * This can either be a reference to a local value, in which case it has `local` set to `true` and + * contains a `ts.Expression`, or it's a reference to an imported value, in which case `local` is + * set to `false` and the symbol and module name of the imported value are provided instead. + * + * If the type is not present or cannot be represented as an expression, `typeValueReference` is + * `null`. */ - typeExpression: ts.Expression|null; + typeValueReference: TypeValueReference|null; /** * TypeScript `ts.TypeNode` representing the type node found in the type position. * - * This field can be used for diagnostics reporting if `typeExpression` is `null`. + * This field can be used for diagnostics reporting if `typeValueReference` is `null`. * * Can be null, if the param has no type declared. */ diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index 708b953965..04aa900f06 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost} from './host'; +import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TypeValueReference} from './host'; /** * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. @@ -48,7 +48,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { // 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; + let typeValueExpr: TypeValueReference|null = null; let originalTypeNode = node.type || null; let typeNode = originalTypeNode; @@ -69,27 +69,57 @@ export class TypeScriptReflectionHost implements ReflectionHost { // It's not possible to get a value expression if the parameter doesn't even have a type. if (typeNode && ts.isTypeReferenceNode(typeNode)) { - // It's only valid to convert a type reference to a value reference if the type actually has - // a value declaration associated with it. - let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(typeNode.typeName); - - if (symbol !== undefined) { - let resolvedSymbol = symbol; - if (symbol.flags & ts.SymbolFlags.Alias) { - resolvedSymbol = this.checker.getAliasedSymbol(symbol); - } - if (resolvedSymbol.valueDeclaration !== undefined) { + const symbols = resolveTypeSymbols(typeNode, this.checker); + if (symbols !== null) { + const {local, decl} = symbols; + // It's only valid to convert a type reference to a value reference if the type actually + // has a value declaration associated with it. + if (decl.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. - const firstDecl = symbol.declarations && symbol.declarations[0]; + + // Look at the local `ts.Symbol`'s declarations and see if it comes from an import + // statement. If so, extract the module specifier and the name of the imported type. + const firstDecl = local.declarations && local.declarations[0]; + if (firstDecl && ts.isImportSpecifier(firstDecl)) { - // Making sure TS produces the necessary imports in case a symbol was declared in a - // different script and imported. To do that we check symbol's first declaration and - // if it's an import - use its identifier. The `Identifier` from the `ImportSpecifier` - // knows it could be a value reference, and will emit as one if needed. - typeValueExpr = ts.updateIdentifier(firstDecl.name); + // The symbol was imported by name, in a ts.ImportSpecifier. + const name = (firstDecl.propertyName || firstDecl.name).text; + const moduleSpecifier = firstDecl.parent.parent.parent.moduleSpecifier; + if (!ts.isStringLiteral(moduleSpecifier)) { + throw new Error('not a module specifier'); + } + const moduleName = moduleSpecifier.text; + typeValueExpr = { + local: false, + name, + moduleName, + valueDeclaration: decl.valueDeclaration, + }; + } else if ( + firstDecl && ts.isNamespaceImport(firstDecl) && symbols.importName !== null) { + // The symbol was imported via a namespace import. In this case, the name to use when + // importing it was extracted by resolveTypeSymbols. + const name = symbols.importName; + const moduleSpecifier = firstDecl.parent.parent.moduleSpecifier; + if (!ts.isStringLiteral(moduleSpecifier)) { + throw new Error('not a module specifier'); + } + const moduleName = moduleSpecifier.text; + typeValueExpr = { + local: false, + name, + moduleName, + valueDeclaration: decl.valueDeclaration, + }; } else { - typeValueExpr = typeNodeToValueExpr(typeNode); + const expression = typeNodeToValueExpr(typeNode); + if (expression !== null) { + typeValueExpr = { + local: true, + expression, + }; + } } } } @@ -98,7 +128,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { return { name, nameNode: node.name, - typeExpression: typeValueExpr, + typeValueReference: typeValueExpr, typeNode: originalTypeNode, decorators, }; }); @@ -486,3 +516,48 @@ function propertyNameToString(node: ts.PropertyName): string|null { return null; } } + +/** + * Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source. + * + * In the event that the `TypeReference` refers to a locally declared symbol, these will be the + * same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved + * `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which + * points to the import statement by which the symbol was imported. + */ +function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker): + {local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null { + const typeName = typeRef.typeName; + // typeRefSymbol is the ts.Symbol of the entire type reference. + const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName); + if (typeRefSymbol === undefined) { + return null; + } + + // local is the ts.Symbol for the local ts.Identifier for the type. + // If the type is actually locally declared or is imported by name, for example: + // import {Foo} from './foo'; + // then it'll be the same as top. If the type is imported via a namespace import, for example: + // import * as foo from './foo'; + // and then referenced as: + // constructor(f: foo.Foo) + // then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`. + // This allows tracking of the import behind whatever type reference exists. + let local = typeRefSymbol; + let importName: string|null = null; + if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) && + ts.isIdentifier(typeName.right)) { + const localTmp = checker.getSymbolAtLocation(typeName.left); + if (localTmp !== undefined) { + local = localTmp; + importName = typeName.right.text; + } + } + + // De-alias the top-level type reference symbol to get the symbol of the actual declaration. + let decl = typeRefSymbol; + if (typeRefSymbol.flags & ts.SymbolFlags.Alias) { + decl = checker.getAliasedSymbol(typeRefSymbol); + } + return {local, decl, importName}; +} diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts index e9e596b434..2425512d61 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts @@ -116,12 +116,38 @@ describe('reflector', () => { const host = new TypeScriptReflectionHost(checker); const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(2); - expectParameter(args[0], 'bar', 'Bar'); - expectParameter(args[1], 'otherBar', 'star.Bar'); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + expectParameter(args[1], 'otherBar', {moduleName: './bar', name: 'Bar'}); }); + it('should reflect an argument from an aliased import', () => { + const {program} = makeProgram([ + { + name: 'bar.ts', + contents: ` + export class Bar {} + ` + }, + { + name: 'entry.ts', + contents: ` + import {Bar as LocalBar} from './bar'; - it('should reflect an nullable argument', () => { + class Foo { + constructor(bar: LocalBar) {} + } + ` + } + ]); + const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration); + const checker = program.getTypeChecker(); + const host = new TypeScriptReflectionHost(checker); + const args = host.getConstructorParameters(clazz) !; + expect(args.length).toBe(1); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); + }); + + it('should reflect a nullable argument', () => { const {program} = makeProgram([ { name: 'bar.ts', @@ -145,7 +171,7 @@ describe('reflector', () => { const host = new TypeScriptReflectionHost(checker); const args = host.getConstructorParameters(clazz) !; expect(args.length).toBe(1); - expectParameter(args[0], 'bar', 'Bar'); + expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); }); }); @@ -193,14 +219,24 @@ describe('reflector', () => { }); function expectParameter( - param: CtorParameter, name: string, type?: string, decorator?: string, - decoratorFrom?: string): void { + param: CtorParameter, name: string, type?: string | {name: string, moduleName: string}, + decorator?: string, decoratorFrom?: string): void { expect(param.name !).toEqual(name); if (type === undefined) { - expect(param.typeExpression).toBeNull(); + expect(param.typeValueReference).toBeNull(); } else { - expect(param.typeExpression).not.toBeNull(); - expect(argExpressionToString(param.typeExpression !)).toEqual(type); + if (param.typeValueReference === null) { + return fail(`Expected parameter ${name} to have a typeValueReference`); + } + if (param.typeValueReference.local && typeof type === 'string') { + expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type); + } else if (!param.typeValueReference.local && typeof type !== 'string') { + expect(param.typeValueReference.moduleName).toEqual(type.moduleName); + expect(param.typeValueReference.name).toEqual(type.name); + } else { + return fail( + `Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.local}`); + } } if (decorator !== undefined) { expect(param.decorators).not.toBeNull(); diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index d256953c43..681a7ac6c7 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -740,8 +740,8 @@ describe('compiler compliance', () => { selectors: [["my-component"]], factory: function MyComponent_Factory(t) { return new (t || MyComponent)( - $r3$.ɵdirectiveInject(ElementRef), $r3$.ɵdirectiveInject(ViewContainerRef), - $r3$.ɵdirectiveInject(ChangeDetectorRef)); + $r3$.ɵdirectiveInject($i$.ElementRef), $r3$.ɵdirectiveInject($i$.ViewContainerRef), + $r3$.ɵdirectiveInject($i$.ChangeDetectorRef)); }, consts: 0, vars: 0, @@ -784,7 +784,7 @@ describe('compiler compliance', () => { IfDirective.ngDirectiveDef = $r3$.ɵdefineDirective({ type: IfDirective, selectors: [["", "if", ""]], - factory: function IfDirective_Factory(t) { return new (t || IfDirective)($r3$.ɵdirectiveInject(TemplateRef)); } + factory: function IfDirective_Factory(t) { return new (t || IfDirective)($r3$.ɵdirectiveInject($i$.TemplateRef)); } });`; const MyComponentDefinition = ` const $c1$ = ["foo", ""]; diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index a19e1c6a3e..3caa96181e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1104,7 +1104,7 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) .toContain( - `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵinjectAttribute("test"), i0.ɵdirectiveInject(ChangeDetectorRef), i0.ɵdirectiveInject(ElementRef), i0.ɵdirectiveInject(Injector), i0.ɵdirectiveInject(Renderer2), i0.ɵdirectiveInject(TemplateRef), i0.ɵdirectiveInject(ViewContainerRef)); }`); + `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵinjectAttribute("test"), i0.ɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵdirectiveInject(i0.ElementRef), i0.ɵdirectiveInject(i0.Injector), i0.ɵdirectiveInject(i0.Renderer2), i0.ɵdirectiveInject(i0.TemplateRef), i0.ɵdirectiveInject(i0.ViewContainerRef)); }`); }); it('should generate queries for components', () => { @@ -1932,9 +1932,9 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import { MyTypeA, MyTypeB } from './types';`); - expect(jsContents).toMatch(setClassMetadataRegExp('type: MyTypeA')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: MyTypeB')); + expect(jsContents).toContain(`import * as i1 from "./types";`); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeA')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeB')); }); it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`', @@ -1961,13 +1961,13 @@ describe('ngtsc behavioral tests', () => { export class SomeComp { constructor(@Inject('arg-token') arg: types.MyTypeB) {} } - `); + `); env.driveMain(); const jsContents = trim(env.getContents('test.js')); expect(jsContents).toContain(`import * as types from './types';`); - expect(jsContents).toMatch(setClassMetadataRegExp('type: types.MyTypeA')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: types.MyTypeB')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeA')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeB')); }); it('should use `undefined` in setClassMetadata if types can\'t be represented as values', () => { diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 6e2c7d555c..c400260081 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -76,7 +76,7 @@ export * from './ml_parser/interpolation_config'; export * from './ml_parser/tags'; export {LexerRange} from './ml_parser/lexer'; export {NgModuleCompiler} from './ng_module_compiler'; -export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, 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, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast'; +export {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, TypeofExpr, collectExternalReferences} from './output/output_ast'; export {EmitterVisitorContext} from './output/abstract_emitter'; export {JitEvaluator} from './output/output_jit'; export * from './output/ts_emitter';