fix(ivy): ngcc - empower `Esm5ReflectionHost` to analyze `ModuleWithProviders` functions (#29092)
In ESM5 code, static methods appear as property assignments onto the constructor function. For example: ``` var MyClass = (function() { function MyClass () {} MyClass.staticMethod = function() {}; return MyClass; })(); ``` This commit teaches ngcc how to process these forms when searching for `ModuleWithProviders` functions that need to be updated in the typings files. PR Close #29092
This commit is contained in:
parent
68f9d705f8
commit
b48d6e1b13
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
|||
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
||||
import {Reference} from '../../../src/ngtsc/imports';
|
||||
import {Declaration} from '../../../src/ngtsc/reflection';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {ModuleWithProvidersFunction, NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {isDefined} from '../utils';
|
||||
|
||||
export interface ModuleWithProvidersInfo {
|
||||
|
@ -38,7 +38,7 @@ export class ModuleWithProvidersAnalyzer {
|
|||
rootFiles.forEach(f => {
|
||||
const fns = this.host.getModuleWithProvidersFunctions(f);
|
||||
fns && fns.forEach(fn => {
|
||||
const dtsFn = this.getDtsDeclaration(fn.declaration);
|
||||
const dtsFn = this.getDtsDeclarationForFunction(fn);
|
||||
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
|
||||
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
|
||||
null;
|
||||
|
@ -82,28 +82,27 @@ export class ModuleWithProvidersAnalyzer {
|
|||
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
||||
}
|
||||
|
||||
private getDtsDeclaration(fn: ts.SignatureDeclaration) {
|
||||
private getDtsDeclarationForFunction(fn: ModuleWithProvidersFunction) {
|
||||
let dtsFn: ts.Declaration|null = null;
|
||||
const containerClass = this.host.getClassSymbol(fn.parent);
|
||||
const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text;
|
||||
if (containerClass && fnName) {
|
||||
const containerClass = fn.container && this.host.getClassSymbol(fn.container);
|
||||
if (containerClass) {
|
||||
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
|
||||
// Get the declaration of the matching static method
|
||||
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
||||
dtsClass.members
|
||||
.find(
|
||||
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
||||
member.name.text === fnName) as ts.Declaration :
|
||||
member.name.text === fn.name) as ts.Declaration :
|
||||
null;
|
||||
} else {
|
||||
dtsFn = this.host.getDtsDeclaration(fn);
|
||||
dtsFn = this.host.getDtsDeclaration(fn.declaration);
|
||||
}
|
||||
if (!dtsFn) {
|
||||
throw new Error(`Matching type declaration for ${fn.getText()} is missing`);
|
||||
throw new Error(`Matching type declaration for ${fn.declaration.getText()} is missing`);
|
||||
}
|
||||
if (!isFunctionOrMethod(dtsFn)) {
|
||||
throw new Error(
|
||||
`Matching type declaration for ${fn.getText()} is not a function: ${dtsFn.getText()}`);
|
||||
`Matching type declaration for ${fn.declaration.getText()} is not a function: ${dtsFn.getText()}`);
|
||||
}
|
||||
return dtsFn;
|
||||
}
|
||||
|
|
|
@ -374,16 +374,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
if (this.isClass(declaration.node)) {
|
||||
this.getMembersOfClass(declaration.node).forEach(member => {
|
||||
if (member.isStatic) {
|
||||
const info = this.parseForModuleWithProviders(member.node);
|
||||
const info = this.parseForModuleWithProviders(
|
||||
member.name, member.node, member.implementation, declaration.node);
|
||||
if (info) {
|
||||
infos.push(info);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const info = this.parseForModuleWithProviders(declaration.node);
|
||||
if (info) {
|
||||
infos.push(info);
|
||||
if (isNamedDeclaration(declaration.node)) {
|
||||
const info =
|
||||
this.parseForModuleWithProviders(declaration.node.name.text, declaration.node);
|
||||
if (info) {
|
||||
infos.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -659,8 +663,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
* Matching statements will look like: `tslib_1.__decorate(...);`.
|
||||
* @param statement the statement that may contain the call.
|
||||
* @param helperName the name of the helper we are looking for.
|
||||
* @returns the node that corresponds to the `__decorate(...)` call or null if the statement does
|
||||
* not match.
|
||||
* @returns the node that corresponds to the `__decorate(...)` call or null if the statement
|
||||
* does not match.
|
||||
*/
|
||||
protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null {
|
||||
if (ts.isExpressionStatement(statement)) {
|
||||
|
@ -1004,7 +1008,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
/**
|
||||
* Search statements related to the given class for calls to the specified helper.
|
||||
* @param classSymbol the class whose helper calls we are interested in.
|
||||
* @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in.
|
||||
* @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested
|
||||
* in.
|
||||
* @returns an array of CallExpression nodes for each matching helper call.
|
||||
*/
|
||||
protected getHelperCallsForClass(classSymbol: ts.Symbol, helperName: string):
|
||||
|
@ -1099,8 +1104,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
* Only the first declaration with a given name is added to the map; subsequent classes will be
|
||||
* ignored.
|
||||
*
|
||||
* We are most interested in classes that are publicly exported from the entry point, so these are
|
||||
* added to the map first, to ensure that they are not ignored.
|
||||
* We are most interested in classes that are publicly exported from the entry point, so these
|
||||
* are added to the map first, to ensure that they are not ignored.
|
||||
*
|
||||
* @param dtsRootFileName The filename of the entry-point to the `dtsTypings` program.
|
||||
* @param dtsProgram The program containing all the typings files.
|
||||
|
@ -1126,15 +1131,23 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
}
|
||||
|
||||
/**
|
||||
* Parse the given node, to see if it is a function that returns a `ModuleWithProviders` object.
|
||||
* @param node a node to check to see if it is a function that returns a `ModuleWithProviders`
|
||||
* object.
|
||||
* Parse a function/method node (or its implementation), to see if it returns a
|
||||
* `ModuleWithProviders` object.
|
||||
* @param name The name of the function.
|
||||
* @param node the node to check - this could be a function, a method or a variable declaration.
|
||||
* @param implementation the actual function expression if `node` is a variable declaration.
|
||||
* @param container the class that contains the function, if it is a method.
|
||||
* @returns info about the function if it does return a `ModuleWithProviders` object; `null`
|
||||
* otherwise.
|
||||
*/
|
||||
protected parseForModuleWithProviders(node: ts.Node|null): ModuleWithProvidersFunction|null {
|
||||
const declaration =
|
||||
node && (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) ? node : null;
|
||||
protected parseForModuleWithProviders(
|
||||
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
|
||||
container: ts.Declaration|null = null): ModuleWithProvidersFunction|null {
|
||||
const declaration = implementation &&
|
||||
(ts.isFunctionDeclaration(implementation) || ts.isMethodDeclaration(implementation) ||
|
||||
ts.isFunctionExpression(implementation)) ?
|
||||
implementation :
|
||||
null;
|
||||
const body = declaration ? this.getDefinitionOfFunction(declaration).body : null;
|
||||
const lastStatement = body && body[body.length - 1];
|
||||
const returnExpression =
|
||||
|
@ -1147,7 +1160,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
|||
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
|
||||
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
|
||||
null;
|
||||
return ngModule && declaration && {ngModule, declaration};
|
||||
return ngModule && declaration && {name, ngModule, declaration, container};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,10 +24,18 @@ export function isSwitchableVariableDeclaration(node: ts.Node):
|
|||
* that return ModuleWithProviders objects.
|
||||
*/
|
||||
export interface ModuleWithProvidersFunction {
|
||||
/**
|
||||
* The name of the declared function.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The declaration of the function that returns the `ModuleWithProviders` object.
|
||||
*/
|
||||
declaration: ts.SignatureDeclaration;
|
||||
/**
|
||||
* Declaration of the containing class (if this is a method)
|
||||
*/
|
||||
container: ts.Declaration|null;
|
||||
/**
|
||||
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
|
||||
*/
|
||||
|
|
|
@ -553,7 +553,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [
|
|||
{name: '/src/module', contents: 'export class ExternalModule {}'},
|
||||
];
|
||||
|
||||
describe('Fesm2015ReflectionHost', () => {
|
||||
describe('Esm2015ReflectionHost', () => {
|
||||
|
||||
describe('getDecoratorsOfDeclaration()', () => {
|
||||
it('should find the decorators on a class', () => {
|
||||
|
|
|
@ -630,6 +630,71 @@ const TYPINGS_DTS_FILES = [
|
|||
{name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`},
|
||||
];
|
||||
|
||||
const MODULE_WITH_PROVIDERS_PROGRAM = [
|
||||
{
|
||||
name: '/src/functions.js',
|
||||
contents: `
|
||||
import {ExternalModule} from './module';
|
||||
|
||||
var SomeService = (function() {
|
||||
function SomeService() {}
|
||||
return SomeService;
|
||||
}());
|
||||
|
||||
var InternalModule = (function() {
|
||||
function InternalModule() {}
|
||||
return InternalModule;
|
||||
}());
|
||||
export function aNumber() { return 42; }
|
||||
export function aString() { return 'foo'; }
|
||||
export function emptyObject() { return {}; }
|
||||
export function ngModuleIdentifier() { return { ngModule: InternalModule }; }
|
||||
export function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; }
|
||||
export function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; }
|
||||
export function onlyProviders() { return { providers: [SomeService] }; }
|
||||
export function ngModuleNumber() { return { ngModule: 42 }; }
|
||||
export function ngModuleString() { return { ngModule: 'foo' }; }
|
||||
export function ngModuleObject() { return { ngModule: { foo: 42 } }; }
|
||||
export function externalNgModule() { return { ngModule: ExternalModule }; }
|
||||
export {SomeService, InternalModule};
|
||||
`
|
||||
},
|
||||
{
|
||||
name: '/src/methods.js',
|
||||
contents: `
|
||||
import {ExternalModule} from './module';
|
||||
var SomeService = (function() {
|
||||
function SomeService() {}
|
||||
return SomeService;
|
||||
}());
|
||||
|
||||
var InternalModule = (function() {
|
||||
function InternalModule() {}
|
||||
InternalModule.prototype = {
|
||||
instanceNgModuleIdentifier: function() { return { ngModule: InternalModule }; },
|
||||
instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; },
|
||||
instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; },
|
||||
instanceExternalNgModule: function() { return { ngModule: ExternalModule }; },
|
||||
};
|
||||
InternalModule.aNumber = function() { return 42; };
|
||||
InternalModule.aString = function() { return 'foo'; };
|
||||
InternalModule.emptyObject = function() { return {}; };
|
||||
InternalModule.ngModuleIdentifier = function() { return { ngModule: InternalModule }; };
|
||||
InternalModule.ngModuleWithEmptyProviders = function() { return { ngModule: InternalModule, providers: [] }; };
|
||||
InternalModule.ngModuleWithProviders = function() { return { ngModule: InternalModule, providers: [SomeService] }; };
|
||||
InternalModule.onlyProviders = function() { return { providers: [SomeService] }; };
|
||||
InternalModule.ngModuleNumber = function() { return { ngModule: 42 }; };
|
||||
InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; };
|
||||
InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; };
|
||||
InternalModule.externalNgModule = function() { return { ngModule: ExternalModule }; };
|
||||
return InternalModule;
|
||||
}());
|
||||
export {SomeService, InternalModule};
|
||||
`
|
||||
},
|
||||
{name: '/src/module', contents: 'export class ExternalModule {}'},
|
||||
];
|
||||
|
||||
describe('Esm5ReflectionHost', () => {
|
||||
|
||||
describe('getDecoratorsOfDeclaration()', () => {
|
||||
|
@ -1744,4 +1809,46 @@ describe('Esm5ReflectionHost', () => {
|
|||
.toEqual('/typings/class2.d.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleWithProvidersFunctions', () => {
|
||||
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
|
||||
() => {
|
||||
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
|
||||
const host = new Esm5ReflectionHost(false, srcProgram.getTypeChecker());
|
||||
const file = srcProgram.getSourceFile('/src/functions.js') !;
|
||||
const fns = host.getModuleWithProvidersFunctions(file);
|
||||
expect(fns.map(info => [info.declaration.name !.getText(), info.ngModule.text])).toEqual([
|
||||
['ngModuleIdentifier', 'InternalModule'],
|
||||
['ngModuleWithEmptyProviders', 'InternalModule'],
|
||||
['ngModuleWithProviders', 'InternalModule'],
|
||||
['externalNgModule', 'ExternalModule'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object',
|
||||
() => {
|
||||
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
|
||||
const host = new Esm5ReflectionHost(false, srcProgram.getTypeChecker());
|
||||
const file = srcProgram.getSourceFile('/src/methods.js') !;
|
||||
const fn = host.getModuleWithProvidersFunctions(file);
|
||||
expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.text])).toEqual([
|
||||
[
|
||||
'function() { return { ngModule: InternalModule }; }',
|
||||
'InternalModule',
|
||||
],
|
||||
[
|
||||
'function() { return { ngModule: InternalModule, providers: [] }; }',
|
||||
'InternalModule',
|
||||
],
|
||||
[
|
||||
'function() { return { ngModule: InternalModule, providers: [SomeService] }; }',
|
||||
'InternalModule',
|
||||
],
|
||||
[
|
||||
'function() { return { ngModule: ExternalModule }; }',
|
||||
'ExternalModule',
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue