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:
Pete Bacon Darwin 2019-03-20 13:47:58 +00:00 committed by Matias Niemelä
parent 68f9d705f8
commit b48d6e1b13
5 changed files with 154 additions and 27 deletions

View File

@ -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;
} }

View File

@ -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};
} }
} }

View File

@ -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.
*/ */

View File

@ -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', () => {

View File

@ -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',
],
]);
});
});
}); });