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 {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
||||||
import {Reference} from '../../../src/ngtsc/imports';
|
import {Reference} from '../../../src/ngtsc/imports';
|
||||||
import {Declaration} from '../../../src/ngtsc/reflection';
|
import {Declaration} from '../../../src/ngtsc/reflection';
|
||||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
import {ModuleWithProvidersFunction, NgccReflectionHost} from '../host/ngcc_host';
|
||||||
import {isDefined} from '../utils';
|
import {isDefined} from '../utils';
|
||||||
|
|
||||||
export interface ModuleWithProvidersInfo {
|
export interface ModuleWithProvidersInfo {
|
||||||
|
@ -38,7 +38,7 @@ export class ModuleWithProvidersAnalyzer {
|
||||||
rootFiles.forEach(f => {
|
rootFiles.forEach(f => {
|
||||||
const fns = this.host.getModuleWithProvidersFunctions(f);
|
const fns = this.host.getModuleWithProvidersFunctions(f);
|
||||||
fns && fns.forEach(fn => {
|
fns && fns.forEach(fn => {
|
||||||
const dtsFn = this.getDtsDeclaration(fn.declaration);
|
const dtsFn = this.getDtsDeclarationForFunction(fn);
|
||||||
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
|
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
|
||||||
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
|
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
|
||||||
null;
|
null;
|
||||||
|
@ -82,28 +82,27 @@ export class ModuleWithProvidersAnalyzer {
|
||||||
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
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;
|
let dtsFn: ts.Declaration|null = null;
|
||||||
const containerClass = this.host.getClassSymbol(fn.parent);
|
const containerClass = fn.container && this.host.getClassSymbol(fn.container);
|
||||||
const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text;
|
if (containerClass) {
|
||||||
if (containerClass && fnName) {
|
|
||||||
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
|
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
|
||||||
// Get the declaration of the matching static method
|
// Get the declaration of the matching static method
|
||||||
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
||||||
dtsClass.members
|
dtsClass.members
|
||||||
.find(
|
.find(
|
||||||
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
||||||
member.name.text === fnName) as ts.Declaration :
|
member.name.text === fn.name) as ts.Declaration :
|
||||||
null;
|
null;
|
||||||
} else {
|
} else {
|
||||||
dtsFn = this.host.getDtsDeclaration(fn);
|
dtsFn = this.host.getDtsDeclaration(fn.declaration);
|
||||||
}
|
}
|
||||||
if (!dtsFn) {
|
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)) {
|
if (!isFunctionOrMethod(dtsFn)) {
|
||||||
throw new Error(
|
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;
|
return dtsFn;
|
||||||
}
|
}
|
||||||
|
|
|
@ -374,16 +374,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||||
if (this.isClass(declaration.node)) {
|
if (this.isClass(declaration.node)) {
|
||||||
this.getMembersOfClass(declaration.node).forEach(member => {
|
this.getMembersOfClass(declaration.node).forEach(member => {
|
||||||
if (member.isStatic) {
|
if (member.isStatic) {
|
||||||
const info = this.parseForModuleWithProviders(member.node);
|
const info = this.parseForModuleWithProviders(
|
||||||
|
member.name, member.node, member.implementation, declaration.node);
|
||||||
if (info) {
|
if (info) {
|
||||||
infos.push(info);
|
infos.push(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const info = this.parseForModuleWithProviders(declaration.node);
|
if (isNamedDeclaration(declaration.node)) {
|
||||||
if (info) {
|
const info =
|
||||||
infos.push(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(...);`.
|
* Matching statements will look like: `tslib_1.__decorate(...);`.
|
||||||
* @param statement the statement that may contain the call.
|
* @param statement the statement that may contain the call.
|
||||||
* @param helperName the name of the helper we are looking for.
|
* @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
|
* @returns the node that corresponds to the `__decorate(...)` call or null if the statement
|
||||||
* not match.
|
* does not match.
|
||||||
*/
|
*/
|
||||||
protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null {
|
protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null {
|
||||||
if (ts.isExpressionStatement(statement)) {
|
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.
|
* 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 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.
|
* @returns an array of CallExpression nodes for each matching helper call.
|
||||||
*/
|
*/
|
||||||
protected getHelperCallsForClass(classSymbol: ts.Symbol, helperName: string):
|
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
|
* Only the first declaration with a given name is added to the map; subsequent classes will be
|
||||||
* ignored.
|
* ignored.
|
||||||
*
|
*
|
||||||
* We are most interested in classes that are publicly exported from the entry point, so these are
|
* We are most interested in classes that are publicly exported from the entry point, so these
|
||||||
* added to the map first, to ensure that they are not ignored.
|
* 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 dtsRootFileName The filename of the entry-point to the `dtsTypings` program.
|
||||||
* @param dtsProgram The program containing all the typings files.
|
* @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.
|
* Parse a function/method node (or its implementation), to see if it returns a
|
||||||
* @param node a node to check to see if it is a function that returns a `ModuleWithProviders`
|
* `ModuleWithProviders` object.
|
||||||
* 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`
|
* @returns info about the function if it does return a `ModuleWithProviders` object; `null`
|
||||||
* otherwise.
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
protected parseForModuleWithProviders(node: ts.Node|null): ModuleWithProvidersFunction|null {
|
protected parseForModuleWithProviders(
|
||||||
const declaration =
|
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
|
||||||
node && (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) ? node : null;
|
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 body = declaration ? this.getDefinitionOfFunction(declaration).body : null;
|
||||||
const lastStatement = body && body[body.length - 1];
|
const lastStatement = body && body[body.length - 1];
|
||||||
const returnExpression =
|
const returnExpression =
|
||||||
|
@ -1147,7 +1160,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
|
||||||
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
|
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
|
||||||
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
|
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
|
||||||
null;
|
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.
|
* that return ModuleWithProviders objects.
|
||||||
*/
|
*/
|
||||||
export interface ModuleWithProvidersFunction {
|
export interface ModuleWithProvidersFunction {
|
||||||
|
/**
|
||||||
|
* The name of the declared function.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The declaration of the function that returns the `ModuleWithProviders` object.
|
* The declaration of the function that returns the `ModuleWithProviders` object.
|
||||||
*/
|
*/
|
||||||
declaration: ts.SignatureDeclaration;
|
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.
|
* 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 {}'},
|
{name: '/src/module', contents: 'export class ExternalModule {}'},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Fesm2015ReflectionHost', () => {
|
describe('Esm2015ReflectionHost', () => {
|
||||||
|
|
||||||
describe('getDecoratorsOfDeclaration()', () => {
|
describe('getDecoratorsOfDeclaration()', () => {
|
||||||
it('should find the decorators on a class', () => {
|
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 {}`},
|
{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('Esm5ReflectionHost', () => {
|
||||||
|
|
||||||
describe('getDecoratorsOfDeclaration()', () => {
|
describe('getDecoratorsOfDeclaration()', () => {
|
||||||
|
@ -1744,4 +1809,46 @@ describe('Esm5ReflectionHost', () => {
|
||||||
.toEqual('/typings/class2.d.ts');
|
.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