fix(ngcc): support inline export declarations in UMD files (#38959)

Previously, any declarations that were defined "inline" were not
recognised by the `UmdReflectionHost`.

For example, the following syntax was completely unrecognized:

```ts
var Foo_1;
exports.Foo = Foo_1 = (function() {
  function Foo() {}
  return Foo;
})();
exports.Foo = Foo_1 = __decorate(SomeDecorator, Foo);
```

Such inline classes were ignored and not processed by ngcc.

This lack of processing led to failures in Ivy applications that relied
on UMD formats of libraries such as `syncfusion/ej2-angular-ui-components`.

Now all known inline UMD exports are recognized and processed accordingly.

Fixes #38947

PR Close #38959
This commit is contained in:
Pete Bacon Darwin 2020-09-29 20:58:57 +01:00 committed by atscott
parent 47eab61cad
commit f4fee86f77
3 changed files with 2565 additions and 2273 deletions

View File

@ -419,10 +419,10 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* @returns An array of class symbols.
*/
findClassSymbols(sourceFile: ts.SourceFile): NgccClassSymbol[] {
const classes: NgccClassSymbol[] = [];
const classes = new Map<ts.Symbol, NgccClassSymbol>();
this.getModuleStatements(sourceFile)
.forEach(statement => this.addClassSymbolsFromStatement(classes, statement));
return classes;
return Array.from(classes.values());
}
/**
@ -462,20 +462,27 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
const decl = this.getDeclarationOfIdentifier(declaration.name);
if (decl === null) {
throw new Error(
`Cannot get the dts file for a node that cannot be associated with a declaration ${
declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
// Try to retrieve the dts declaration from the public map
if (this.publicDtsDeclarationMap === null) {
this.publicDtsDeclarationMap = this.computePublicDtsDeclarationMap(this.src, this.dts);
}
if (this.publicDtsDeclarationMap.has(declaration)) {
return this.publicDtsDeclarationMap.get(declaration)!;
if (this.publicDtsDeclarationMap.has(decl.node)) {
return this.publicDtsDeclarationMap.get(decl.node)!;
}
// No public export, try the private map
if (this.privateDtsDeclarationMap === null) {
this.privateDtsDeclarationMap = this.computePrivateDtsDeclarationMap(this.src, this.dts);
}
if (this.privateDtsDeclarationMap.has(declaration)) {
return this.privateDtsDeclarationMap.get(declaration)!;
if (this.privateDtsDeclarationMap.has(decl.node)) {
return this.privateDtsDeclarationMap.get(decl.node)!;
}
// No declaration found at all
@ -547,21 +554,21 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
///////////// Protected Helpers /////////////
/**
* Extract all the "classes" from the `statement` and add them to the `classes` array.
* Extract all the "classes" from the `statement` and add them to the `classes` map.
*/
protected addClassSymbolsFromStatement(classes: NgccClassSymbol[], statement: ts.Statement):
void {
protected addClassSymbolsFromStatement(
classes: Map<ts.Symbol, NgccClassSymbol>, statement: ts.Statement): void {
if (ts.isVariableStatement(statement)) {
statement.declarationList.declarations.forEach(declaration => {
const classSymbol = this.getClassSymbol(declaration);
if (classSymbol) {
classes.push(classSymbol);
classes.set(classSymbol.implementation, classSymbol);
}
});
} else if (ts.isClassDeclaration(statement)) {
const classSymbol = this.getClassSymbol(statement);
if (classSymbol) {
classes.push(classSymbol);
classes.set(classSymbol.implementation, classSymbol);
}
}
}

View File

@ -10,13 +10,14 @@ import * as ts from 'typescript';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {Declaration, DeclarationKind, Import} from '../../../src/ngtsc/reflection';
import {Declaration, DeclarationKind, Import, isNamedFunctionDeclaration} from '../../../src/ngtsc/reflection';
import {BundleProgram} from '../packages/bundle_program';
import {FactoryMap, getTsHelperFnFromIdentifier, stripExtension} from '../utils';
import {DefinePropertyReexportStatement, ExportDeclaration, ExportsStatement, extractGetterFnExpression, findNamespaceOfIdentifier, findRequireCallReference, isDefinePropertyReexportStatement, isExportsStatement, isExternalImport, isRequireCall, isWildcardReexportStatement, WildcardReexportStatement} from './commonjs_umd_utils';
import {isAssignment} from './esm2015_host';
import {DefinePropertyReexportStatement, ExportDeclaration, ExportsStatement, extractGetterFnExpression, findNamespaceOfIdentifier, findRequireCallReference, isDefinePropertyReexportStatement, isExportsAssignment, isExportsDeclaration, isExportsStatement, isExternalImport, isRequireCall, isWildcardReexportStatement, WildcardReexportStatement} from './commonjs_umd_utils';
import {getInnerClassDeclaration, getOuterNodeFromInnerDeclaration, isAssignment} from './esm2015_host';
import {Esm5ReflectionHost} from './esm5_host';
import {NgccClassSymbol} from './ngcc_host';
import {stripParentheses} from './utils';
export class UmdReflectionHost extends Esm5ReflectionHost {
@ -45,8 +46,40 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
}
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
return this.getExportsDeclaration(id) || this.getUmdModuleDeclaration(id) ||
this.getUmdDeclaration(id) || super.getDeclarationOfIdentifier(id);
// First we try one of the the following:
// 1. The `exports` identifier - referring to the current file/module.
// 2. An identifier (e.g. `foo`) that refers to an imported UMD module.
// 3. A UMD style export identifier (e.g. the `foo` of `exports.foo`).
const declaration = this.getExportsDeclaration(id) || this.getUmdModuleDeclaration(id) ||
this.getUmdDeclaration(id);
if (declaration !== null) {
return declaration;
}
// Try to get the declaration using the super class.
const superDeclaration = super.getDeclarationOfIdentifier(id);
if (superDeclaration === null) {
return null;
}
// Check to see if the declaration is the inner node of a declaration IIFE.
const outerNode = getOuterNodeFromInnerDeclaration(superDeclaration.node);
if (outerNode === null) {
return superDeclaration;
}
// We are only interested if the outer declaration is of the form
// `exports.<name> = <initializer>`.
if (!isExportsAssignment(outerNode)) {
return superDeclaration;
}
return {
kind: DeclarationKind.Inline,
node: outerNode.left,
known: null,
viaModule: null,
};
}
getExportsOfModule(module: ts.Node): Map<string, Declaration>|null {
@ -78,6 +111,102 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
return umdModule !== null ? Array.from(umdModule.factoryFn.body.statements) : [];
}
protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
const superSymbol = super.getClassSymbolFromOuterDeclaration(declaration);
if (superSymbol) {
return superSymbol;
}
if (!isExportsDeclaration(declaration)) {
return undefined;
}
let initializer = skipAliases(declaration.parent.right);
if (ts.isIdentifier(initializer)) {
const implementation = this.getDeclarationOfIdentifier(initializer);
if (implementation !== null) {
const implementationSymbol = this.getClassSymbol(implementation.node);
if (implementationSymbol !== null) {
return implementationSymbol;
}
}
}
const innerDeclaration = getInnerClassDeclaration(initializer);
if (innerDeclaration !== null) {
return this.createClassSymbol(declaration.name, innerDeclaration);
}
return undefined;
}
protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined {
const superClassSymbol = super.getClassSymbolFromInnerDeclaration(declaration);
if (superClassSymbol !== undefined) {
return superClassSymbol;
}
if (!isNamedFunctionDeclaration(declaration)) {
return undefined;
}
const outerNode = getOuterNodeFromInnerDeclaration(declaration);
if (outerNode === null || !isExportsAssignment(outerNode)) {
return undefined;
}
return this.createClassSymbol(outerNode.left.name, declaration);
}
/**
* Extract all "classes" from the `statement` and add them to the `classes` map.
*/
protected addClassSymbolsFromStatement(
classes: Map<ts.Symbol, NgccClassSymbol>, statement: ts.Statement): void {
super.addClassSymbolsFromStatement(classes, statement);
// Also check for exports of the form: `exports.<name> = <class def>;`
if (isExportsStatement(statement)) {
const classSymbol = this.getClassSymbol(statement.expression.left);
if (classSymbol) {
classes.set(classSymbol.implementation, classSymbol);
}
}
}
/**
* Analyze the given statement to see if it corresponds with an exports declaration like
* `exports.MyClass = MyClass_1 = <class def>;`. If so, the declaration of `MyClass_1`
* is associated with the `MyClass` identifier.
*
* @param statement The statement that needs to be preprocessed.
*/
protected preprocessStatement(statement: ts.Statement): void {
super.preprocessStatement(statement);
if (!isExportsStatement(statement)) {
return;
}
const declaration = statement.expression.left;
const initializer = statement.expression.right;
if (!isAssignment(initializer) || !ts.isIdentifier(initializer.left) ||
!this.isClass(declaration)) {
return;
}
const aliasedIdentifier = initializer.left;
const aliasedDeclaration = this.getDeclarationOfIdentifier(aliasedIdentifier);
if (aliasedDeclaration === null || aliasedDeclaration.node === null) {
throw new Error(
`Unable to locate declaration of ${aliasedIdentifier.text} in "${statement.getText()}"`);
}
this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name);
}
private computeUmdModule(sourceFile: ts.SourceFile): UmdModule|null {
if (sourceFile.statements.length !== 1) {
throw new Error(
@ -93,7 +222,17 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
for (const statement of this.getModuleStatements(sourceFile)) {
if (isExportsStatement(statement)) {
const exportDeclaration = this.extractBasicUmdExportDeclaration(statement);
moduleMap.set(exportDeclaration.name, exportDeclaration.declaration);
if (!moduleMap.has(exportDeclaration.name)) {
// We assume that the first `exports.<name>` is the actual declaration, and that any
// subsequent statements that match are decorating the original declaration.
// For example:
// ```
// exports.foo = <declaration>;
// exports.foo = __decorate(<decorator>, exports.foo);
// ```
// The declaration is the first line not the second.
moduleMap.set(exportDeclaration.name, exportDeclaration.declaration);
}
} else if (isWildcardReexportStatement(statement)) {
const reexports = this.extractUmdWildcardReexports(statement, sourceFile);
for (const reexport of reexports) {
@ -136,7 +275,13 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
private extractBasicUmdExportDeclaration(statement: ExportsStatement): ExportDeclaration {
const name = statement.expression.left.name.text;
const exportExpression = skipAliases(statement.expression.right);
return this.extractUmdExportDeclaration(name, exportExpression);
const declaration = this.getDeclarationOfExpression(exportExpression) ?? {
kind: DeclarationKind.Inline,
node: statement.expression.left,
known: null,
viaModule: null,
};
return {name, declaration};
}
private extractUmdWildcardReexports(
@ -185,17 +330,20 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
if (getterFnExpression === null) {
return null;
}
return this.extractUmdExportDeclaration(name, getterFnExpression);
}
private extractUmdExportDeclaration(name: string, expression: ts.Expression): ExportDeclaration {
const declaration = this.getDeclarationOfExpression(expression);
const declaration = this.getDeclarationOfExpression(getterFnExpression);
if (declaration !== null) {
return {name, declaration};
}
return {
name,
declaration: {kind: DeclarationKind.Inline, node: expression, known: null, viaModule: null},
declaration: {
kind: DeclarationKind.Inline,
node: getterFnExpression,
known: null,
viaModule: null,
},
};
}
@ -214,6 +362,21 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
if (nsIdentifier === null) {
return null;
}
if (nsIdentifier.parent.parent && isExportsAssignment(nsIdentifier.parent.parent)) {
const initializer = nsIdentifier.parent.parent.right;
if (ts.isIdentifier(initializer)) {
return this.getDeclarationOfIdentifier(initializer);
}
return this.detectKnownDeclaration({
kind: DeclarationKind.Inline,
node: nsIdentifier.parent.parent.left,
implementation: skipAliases(nsIdentifier.parent.parent.right),
viaModule: null,
known: null,
});
}
const moduleDeclaration = this.getUmdModuleDeclaration(nsIdentifier);
if (moduleDeclaration === null || moduleDeclaration.node === null ||
!ts.isSourceFile(moduleDeclaration.node)) {
@ -242,7 +405,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
}
private getExportsDeclaration(id: ts.Identifier): Declaration|null {
if (!isExportIdentifier(id)) {
if (!isExportsIdentifier(id)) {
return null;
}
@ -398,7 +561,7 @@ function getRequiredModulePath(wrapperFn: ts.FunctionExpression, paramIndex: num
/**
* Is the `node` an identifier with the name "exports"?
*/
export function isExportIdentifier(node: ts.Node): node is ts.Identifier {
function isExportsIdentifier(node: ts.Node): node is ts.Identifier {
return ts.isIdentifier(node) && node.text === 'exports';
}
@ -412,7 +575,7 @@ export function isExportIdentifier(node: ts.Node): node is ts.Identifier {
* @param node the expression to parse
* @returns the original `node` or the far right expression of a series of assignments.
*/
export function skipAliases(node: ts.Expression): ts.Expression {
function skipAliases(node: ts.Expression): ts.Expression {
while (isAssignment(node)) {
node = node.right;
}

File diff suppressed because it is too large Load Diff