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
This commit is contained in:
parent
8bfaaf164a
commit
63013f1aeb
@ -286,12 +286,19 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
|||||||
*/
|
*/
|
||||||
private _reflectModuleFromTypeParam(type: ts.TypeNode): ts.Expression|null {
|
private _reflectModuleFromTypeParam(type: ts.TypeNode): ts.Expression|null {
|
||||||
// Examine the type of the function to see if it's a ModuleWithProviders reference.
|
// Examine the type of the function to see if it's a ModuleWithProviders reference.
|
||||||
if (!ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName)) {
|
if (!ts.isTypeReferenceNode(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeName = type && (ts.isIdentifier(type.typeName) && type.typeName ||
|
||||||
|
ts.isQualifiedName(type.typeName) && type.typeName.right) ||
|
||||||
|
null;
|
||||||
|
if (typeName === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look at the type itself to see where it comes from.
|
// Look at the type itself to see where it comes from.
|
||||||
const id = this.reflector.getImportOfIdentifier(type.typeName);
|
const id = this.reflector.getImportOfIdentifier(typeName);
|
||||||
|
|
||||||
// If it's not named ModuleWithProviders, bail.
|
// If it's not named ModuleWithProviders, bail.
|
||||||
if (id === null || id.name !== 'ModuleWithProviders') {
|
if (id === null || id.name !== 'ModuleWithProviders') {
|
||||||
|
@ -79,35 +79,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getImportOfIdentifier(id: ts.Identifier): Import|null {
|
getImportOfIdentifier(id: ts.Identifier): Import|null {
|
||||||
const symbol = this.checker.getSymbolAtLocation(id);
|
return this.getDirectImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(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};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getExportsOfModule(node: ts.Node): Map<string, Declaration>|null {
|
getExportsOfModule(node: ts.Node): Map<string, Declaration>|null {
|
||||||
@ -179,6 +151,90 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
|||||||
|
|
||||||
getDtsDeclaration(_: ts.Declaration): ts.Declaration|null { return null; }
|
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.
|
* 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;
|
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;
|
||||||
|
}
|
||||||
|
@ -1277,7 +1277,7 @@ describe('ngtsc behavioral tests', () => {
|
|||||||
env.tsconfig();
|
env.tsconfig();
|
||||||
env.write('dep.d.ts', `
|
env.write('dep.d.ts', `
|
||||||
import {ModuleWithProviders, ɵNgModuleDefWithMeta as NgModuleDefWithMeta} from '@angular/core';
|
import {ModuleWithProviders, ɵNgModuleDefWithMeta as NgModuleDefWithMeta} from '@angular/core';
|
||||||
|
|
||||||
export declare class DepModule {
|
export declare class DepModule {
|
||||||
static forRoot(arg1: any, arg2: any): ModuleWithProviders<DepModule>;
|
static forRoot(arg1: any, arg2: any): ModuleWithProviders<DepModule>;
|
||||||
static ngModuleDef: NgModuleDefWithMeta<DepModule, never, never, never>;
|
static ngModuleDef: NgModuleDefWithMeta<DepModule, never, never, never>;
|
||||||
@ -1286,12 +1286,12 @@ describe('ngtsc behavioral tests', () => {
|
|||||||
env.write('test.ts', `
|
env.write('test.ts', `
|
||||||
import {NgModule, ModuleWithProviders} from '@angular/core';
|
import {NgModule, ModuleWithProviders} from '@angular/core';
|
||||||
import {DepModule} from './dep';
|
import {DepModule} from './dep';
|
||||||
|
|
||||||
@NgModule({})
|
@NgModule({})
|
||||||
export class Base {}
|
export class Base {}
|
||||||
|
|
||||||
const mwp = DepModule.forRoot(1,2);
|
const mwp = DepModule.forRoot(1,2);
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [mwp],
|
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',
|
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<RouterModule, never, never, never>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
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<TestModule, never, [typeof i1.RouterModule], never>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it',
|
||||||
() => {
|
() => {
|
||||||
env.tsconfig();
|
env.tsconfig();
|
||||||
env.write(`test.ts`, `
|
env.write(`test.ts`, `
|
||||||
@ -1315,12 +1349,11 @@ describe('ngtsc behavioral tests', () => {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
env.write('node_modules/router/index.d.ts', `
|
env.write('node_modules/router/index.d.ts', `
|
||||||
import {ModuleWithProviders, ɵNgModuleDefWithMeta} from '@angular/core';
|
import * as core from '@angular/core';
|
||||||
|
import {RouterModule} from 'router';
|
||||||
export interface MyType extends ModuleWithProviders {}
|
|
||||||
|
|
||||||
declare class RouterModule {
|
declare class RouterModule {
|
||||||
static forRoot(): (MyType)&{ngModule:RouterModule};
|
static forRoot(): core.ModuleWithProviders<RouterModule>;
|
||||||
static ngModuleDef: ɵNgModuleDefWithMeta<RouterModule, never, never, never>;
|
static ngModuleDef: ɵNgModuleDefWithMeta<RouterModule, never, never, never>;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user