From 63013f1aebfc2bc468fd2ef3f4b4f6d0fa04f8bb Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 11 Dec 2018 13:55:45 +0000 Subject: [PATCH] fix(ivy): support finding the import of namespace-imported identifiers (#27675) Currently there is no support in ngtsc for imports of the form: ``` import * as core from `@angular/core` export function forRoot(): core.ModuleWithProviders; ``` This commit modifies the `ReflectionHost.getImportOfIdentifier(id)` method, so that it supports this kind of return type. PR Close #27675 --- .../src/ngtsc/annotations/src/ng_module.ts | 11 +- .../src/ngtsc/reflection/src/typescript.ts | 127 ++++++++++++++---- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 49 +++++-- 3 files changed, 148 insertions(+), 39 deletions(-) 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 737fd91856..3cec047fd8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -286,12 +286,19 @@ export class NgModuleDecoratorHandler implements DecoratorHandler|null { @@ -179,6 +151,90 @@ export class TypeScriptReflectionHost implements ReflectionHost { getDtsDeclaration(_: ts.Declaration): ts.Declaration|null { return null; } + + protected getDirectImportOfIdentifier(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}; + } + + /** + * Try to get the import info for this identifier as though it is a namespaced import. + * For example, if the identifier is the `Directive` part of a qualified type chain like: + * + * ``` + * core.Directive + * ``` + * + * then it might be that `core` is a namespace import such as: + * + * ``` + * import * as core from 'tslib'; + * ``` + * + * @param id the TypeScript identifier to find the import info for. + * @returns The import info if this is a namespaced import or `null`. + */ + protected getImportOfNamespacedIdentifier(id: ts.Identifier): Import|null { + if (!(ts.isQualifiedName(id.parent) && id.parent.right === id)) { + return null; + } + const namespaceIdentifier = getQualifiedNameRoot(id.parent); + if (!namespaceIdentifier) { + return null; + } + const namespaceSymbol = this.checker.getSymbolAtLocation(namespaceIdentifier); + if (!namespaceSymbol) { + return null; + } + const declaration = + namespaceSymbol.declarations.length === 1 ? namespaceSymbol.declarations[0] : null; + if (!declaration) { + return null; + } + const namespaceDeclaration = ts.isNamespaceImport(declaration) ? declaration : null; + if (!namespaceDeclaration) { + return null; + } + + const importDeclaration = namespaceDeclaration.parent.parent; + if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) { + // Should not happen as this would be invalid TypesScript + return null; + } + + return { + from: importDeclaration.moduleSpecifier.text, + name: id.text, + }; + } + /** * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. * @@ -443,3 +499,16 @@ function propertyNameToString(node: ts.PropertyName): string|null { return null; } } + +/** + * Compute the left most identifier in a qualified type chain. E.g. the `a` of `a.b.c.SomeType`. + * @param qualifiedName The starting property access expression from which we want to compute + * the left most identifier. + * @returns the left most identifier in the chain or `null` if it is not an identifier. + */ +function getQualifiedNameRoot(qualifiedName: ts.QualifiedName): ts.Identifier|null { + while (ts.isQualifiedName(qualifiedName.left)) { + qualifiedName = qualifiedName.left; + } + return ts.isIdentifier(qualifiedName.left) ? qualifiedName.left : null; +} diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 8f9521ccbe..e05903abf2 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1277,7 +1277,7 @@ describe('ngtsc behavioral tests', () => { env.tsconfig(); env.write('dep.d.ts', ` import {ModuleWithProviders, ɵNgModuleDefWithMeta as NgModuleDefWithMeta} from '@angular/core'; - + export declare class DepModule { static forRoot(arg1: any, arg2: any): ModuleWithProviders; static ngModuleDef: NgModuleDefWithMeta; @@ -1286,12 +1286,12 @@ describe('ngtsc behavioral tests', () => { env.write('test.ts', ` import {NgModule, ModuleWithProviders} from '@angular/core'; import {DepModule} from './dep'; - + @NgModule({}) export class Base {} - + const mwp = DepModule.forRoot(1,2); - + @NgModule({ imports: [mwp], }) @@ -1304,6 +1304,40 @@ describe('ngtsc behavioral tests', () => { }); it('should unwrap a ModuleWithProviders-like function if a matching literal type is provided for it', + () => { + env.tsconfig(); + env.write(`test.ts`, ` + import {NgModule} from '@angular/core'; + import {RouterModule} from 'router'; + + @NgModule({imports: [RouterModule.forRoot()]}) + export class TestModule {} + `); + + env.write('node_modules/router/index.d.ts', ` + import {ModuleWithProviders, ɵNgModuleDefWithMeta} from '@angular/core'; + + export interface MyType extends ModuleWithProviders {} + + declare class RouterModule { + static forRoot(): (MyType)&{ngModule:RouterModule}; + static ngModuleDef: ɵNgModuleDefWithMeta; + } + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [[RouterModule.forRoot()]]'); + + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents) + .toContain( + 'i0.ɵNgModuleDefWithMeta'); + }); + + it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', () => { env.tsconfig(); env.write(`test.ts`, ` @@ -1315,12 +1349,11 @@ describe('ngtsc behavioral tests', () => { `); env.write('node_modules/router/index.d.ts', ` - import {ModuleWithProviders, ɵNgModuleDefWithMeta} from '@angular/core'; - - export interface MyType extends ModuleWithProviders {} + import * as core from '@angular/core'; + import {RouterModule} from 'router'; declare class RouterModule { - static forRoot(): (MyType)&{ngModule:RouterModule}; + static forRoot(): core.ModuleWithProviders; static ngModuleDef: ɵNgModuleDefWithMeta; } `);