refactor(ivy): include generic type for ModuleWithProviders in .d.ts files (#34235)

The `ModuleWithProviders` type has an optional type parameter that
should be specified to indicate what NgModule class will be provided.
This enables the Ivy compiler to statically determine the NgModule type
from the declaration files. This type parameter will become required in
the future, however to aid in the migration the compiler will detect
code patterns where using `ModuleWithProviders` as return type is
appropriate, in which case it transforms the emitted .d.ts files to
include the generic type argument.

This should reduce the number of occurrences where `ModuleWithProviders`
is referenced without its generic type argument.

Resolves FW-389

PR Close #34235
This commit is contained in:
JoostK
2019-12-03 21:25:27 +01:00
committed by Andrew Kushnir
parent a8fced8846
commit b72c7a89a9
12 changed files with 592 additions and 12 deletions

View File

@ -13,6 +13,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",

View File

@ -8,5 +8,5 @@
export * from './src/api';
export {IvyCompilation} from './src/compilation';
export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform} from './src/declaration';
export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration';
export {ivyTransformFactory} from './src/transform';

View File

@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import {ConstantPool, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportRewriter} from '../../imports';
import {IncrementalDriver} from '../../incremental';
import {IndexingContext} from '../../indexer';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
import {PerfRecorder} from '../../perf';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope';
@ -71,7 +72,8 @@ export class IvyCompilation {
private importRewriter: ImportRewriter, private incrementalDriver: IncrementalDriver,
private perf: PerfRecorder, private sourceToFactorySymbols: Map<string, Set<string>>|null,
private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean,
private dtsTransforms: DtsTransformRegistry) {}
private dtsTransforms: DtsTransformRegistry, private mwpScanner: ModuleWithProvidersScanner) {
}
get exportStatements(): Map<string, Map<string, [string, string]>> { return this.reexportMap; }
@ -235,6 +237,14 @@ export class IvyCompilation {
visit(sf);
this.mwpScanner.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
// Only obtain the return type transform for the source file once there's a type to replace,
// so that no transform is allocated when there's nothing to do.
this.dtsTransforms.getReturnTypeTransform(sf).addTypeReplacement(node, type);
}
});
if (preanalyze && promises.length > 0) {
return Promise.all(promises).then(() => undefined);
} else {

View File

@ -21,13 +21,21 @@ import {addImports} from './utils';
* have their declaration file transformed.
*/
export class DtsTransformRegistry {
private ivyDeclarationTransforms = new Map<string, IvyDeclarationDtsTransform>();
private ivyDeclarationTransforms = new Map<ts.SourceFile, IvyDeclarationDtsTransform>();
private returnTypeTransforms = new Map<ts.SourceFile, ReturnTypeTransform>();
getIvyDeclarationTransform(sf: ts.SourceFile): IvyDeclarationDtsTransform {
if (!this.ivyDeclarationTransforms.has(sf.fileName)) {
this.ivyDeclarationTransforms.set(sf.fileName, new IvyDeclarationDtsTransform());
if (!this.ivyDeclarationTransforms.has(sf)) {
this.ivyDeclarationTransforms.set(sf, new IvyDeclarationDtsTransform());
}
return this.ivyDeclarationTransforms.get(sf.fileName) !;
return this.ivyDeclarationTransforms.get(sf) !;
}
getReturnTypeTransform(sf: ts.SourceFile): ReturnTypeTransform {
if (!this.returnTypeTransforms.has(sf)) {
this.returnTypeTransforms.set(sf, new ReturnTypeTransform());
}
return this.returnTypeTransforms.get(sf) !;
}
/**
@ -42,11 +50,16 @@ export class DtsTransformRegistry {
if (!sf.isDeclarationFile) {
return null;
}
const originalSf = ts.getOriginalNode(sf) as ts.SourceFile;
let transforms: DtsTransform[]|null = null;
if (this.ivyDeclarationTransforms.has(sf.fileName)) {
if (this.ivyDeclarationTransforms.has(originalSf)) {
transforms = [];
transforms.push(this.ivyDeclarationTransforms.get(sf.fileName) !);
transforms.push(this.ivyDeclarationTransforms.get(originalSf) !);
}
if (this.returnTypeTransforms.has(originalSf)) {
transforms = transforms || [];
transforms.push(this.returnTypeTransforms.get(originalSf) !);
}
return transforms;
}
@ -211,3 +224,61 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
/* members */[...members, ...newMembers]);
}
}
export class ReturnTypeTransform implements DtsTransform {
private typeReplacements = new Map<ts.Declaration, Type>();
addTypeReplacement(declaration: ts.Declaration, type: Type): void {
this.typeReplacements.set(declaration, type);
}
transformClassElement(element: ts.ClassElement, imports: ImportManager): ts.ClassElement {
if (!ts.isMethodSignature(element)) {
return element;
}
const original = ts.getOriginalNode(element) as ts.MethodDeclaration;
if (!this.typeReplacements.has(original)) {
return element;
}
const returnType = this.typeReplacements.get(original) !;
const tsReturnType = translateType(returnType, imports);
const methodSignature = ts.updateMethodSignature(
/* node */ element,
/* typeParameters */ element.typeParameters,
/* parameters */ element.parameters,
/* type */ tsReturnType,
/* name */ element.name,
/* questionToken */ element.questionToken);
// Copy over any modifiers, these cannot be set during the `ts.updateMethodSignature` call.
methodSignature.modifiers = element.modifiers;
// A bug in the TypeScript declaration causes `ts.MethodSignature` not to be assignable to
// `ts.ClassElement`. Since `element` was a `ts.MethodSignature` already, transforming it into
// this type is actually correct.
return methodSignature as unknown as ts.ClassElement;
}
transformFunctionDeclaration(element: ts.FunctionDeclaration, imports: ImportManager):
ts.FunctionDeclaration {
const original = ts.getOriginalNode(element) as ts.FunctionDeclaration;
if (!this.typeReplacements.has(original)) {
return element;
}
const returnType = this.typeReplacements.get(original) !;
const tsReturnType = translateType(returnType, imports);
return ts.updateFunctionDeclaration(
/* node */ element,
/* decorators */ element.decorators,
/* modifiers */ element.modifiers,
/* asteriskToken */ element.asteriskToken,
/* name */ element.name,
/* typeParameters */ element.typeParameters,
/* parameters */ element.parameters,
/* type */ tsReturnType,
/* body */ element.body);
}
}