fix(ngcc): correctly detect emitted TS helpers in ES5 (#35191)

In ES5 code, TypeScript requires certain helpers (such as
`__spreadArrays()`) to be able to support ES2015+ features. These
helpers can be either imported from `tslib` (by setting the
`importHelpers` TS compiler option to `true`) or emitted inline (by
setting the `importHelpers` and `noEmitHelpers` TS compiler options to
`false`, which is the default value for both).

Ngtsc's `StaticInterpreter` (which is also used during ngcc processing)
is able to statically evaluate some of these helpers (currently
`__assign()`, `__spread()` and `__spreadArrays()`), as long as
`ReflectionHost#getDefinitionOfFunction()` correctly detects the
declaration of the helper. For this to happen, the left-hand side of the
corresponding call expression (i.e. `__spread(...)` or
`tslib.__spread(...)`) must be evaluated as a function declaration for
`getDefinitionOfFunction()` to be called with.

In the case of imported helpers, the `tslib.__someHelper` expression was
resolved to a function declaration of the form
`export declare function __someHelper(...args: any[][]): any[];`, which
allows `getDefinitionOfFunction()` to correctly map it to a TS helper.

In contrast, in the case of emitted helpers (and regardless of the
module format: `CommonJS`, `ESNext`, `UMD`, etc.)), the `__someHelper`
identifier was resolved to a variable declaration of the form
`var __someHelper = (this && this.__someHelper) || function () { ... }`,
which upon further evaluation was categorized as a `DynamicValue`
(prohibiting further evaluation by the `getDefinitionOfFunction()`).

As a result of the above, emitted TypeScript helpers were not evaluated
in ES5 code.

---
This commit changes the detection of TS helpers to leverage the existing
`KnownFn` feature (previously only used for built-in functions).
`Esm5ReflectionHost` is changed to always return `KnownDeclaration`s for
TS helpers, both imported (`getExportsOfModule()`) as well as emitted
(`getDeclarationOfIdentifier()`).

Similar changes are made to `CommonJsReflectionHost` and
`UmdReflectionHost`.

The `KnownDeclaration`s are then mapped to `KnownFn`s in
`StaticInterpreter`, allowing it to statically evaluate call expressions
involving any kind of TS helpers.

Jira issue: https://angular-team.atlassian.net/browse/FW-1689

PR Close #35191
This commit is contained in:
George Kalpakas
2020-02-06 18:44:49 +02:00
committed by Miško Hevery
parent 14744f27c5
commit bd6a39c364
15 changed files with 1194 additions and 453 deletions

View File

@ -11,7 +11,7 @@ import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {FactoryMap, isDefined, stripExtension} from '../utils';
import {FactoryMap, getTsHelperFnFromIdentifier, isDefined, stripExtension} from '../utils';
import {ExportDeclaration, ExportStatement, ReexportStatement, RequireCall, findNamespaceOfIdentifier, findRequireCallReference, isExportStatement, isReexportStatement, isRequireCall} from './commonjs_umd_utils';
import {Esm5ReflectionHost} from './esm5_host';
@ -189,7 +189,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
}
const viaModule = !importInfo.from.startsWith('.') ? importInfo.from : null;
return {node: importedFile, known: null, viaModule};
return {node: importedFile, known: getTsHelperFnFromIdentifier(id), viaModule};
}
private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile

View File

@ -8,8 +8,8 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, TsHelperFn, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, isNamedVariableDeclaration, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, getTsHelperFnFromDeclaration, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement} from './esm2015_host';
import {NgccClassSymbol} from './ngcc_host';
@ -118,6 +118,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
// Return the statement before the IIFE return statement
return iifeBody.statements[returnStatementIndex - 1];
}
/**
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE,
* whose value is assigned to a variable (which represents the class to the rest of the program).
@ -245,23 +246,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
*/
getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null {
if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) &&
!ts.isFunctionExpression(node) && !ts.isVariableDeclaration(node)) {
return null;
}
const tsHelperFn = getTsHelperFn(node);
if (tsHelperFn !== null) {
return {
node,
body: null,
helper: tsHelperFn,
parameters: [],
};
}
// If the node was not identified to be a TypeScript helper, a variable declaration at this
// point cannot be resolved as a function.
if (ts.isVariableDeclaration(node)) {
!ts.isFunctionExpression(node)) {
return null;
}
@ -276,11 +261,26 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return !lookingForParamInitializers;
});
return {node, body: statements || null, helper: null, parameters};
return {node, body: statements || null, parameters};
}
///////////// Protected Helpers /////////////
/**
* Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known
* TypeScript helper function.
*/
protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration
|null {
const superDeclaration = super.getDeclarationOfSymbol(symbol, originalId);
if (superDeclaration !== null && superDeclaration.node !== null &&
superDeclaration.known === null) {
superDeclaration.known = getTsHelperFnFromDeclaration(superDeclaration.node);
}
return superDeclaration;
}
/**
* Get the inner function declaration of an ES5-style class.
@ -651,28 +651,6 @@ function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
/**
* Inspects a function declaration to determine if it corresponds with a TypeScript helper function,
* returning its kind if so or null if the declaration does not seem to correspond with such a
* helper.
*/
function getTsHelperFn(node: ts.NamedDeclaration): TsHelperFn|null {
const name = node.name !== undefined && ts.isIdentifier(node.name) ?
stripDollarSuffix(node.name.text) :
null;
switch (name) {
case '__assign':
return TsHelperFn.Assign;
case '__spread':
return TsHelperFn.Spread;
case '__spreadArrays':
return TsHelperFn.SpreadArrays;
default:
return null;
}
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.

View File

@ -12,7 +12,7 @@ import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {FactoryMap, stripExtension} from '../utils';
import {FactoryMap, getTsHelperFnFromIdentifier, stripExtension} from '../utils';
import {ExportDeclaration, ExportStatement, ReexportStatement, findNamespaceOfIdentifier, findRequireCallReference, isExportStatement, isReexportStatement, isRequireCall} from './commonjs_umd_utils';
import {Esm5ReflectionHost, stripParentheses} from './esm5_host';
@ -215,7 +215,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
// We need to add the `viaModule` because the `getExportsOfModule()` call
// did not know that we were importing the declaration.
return {node: importedFile, known: null, viaModule: importInfo.from};
return {node: importedFile, known: getTsHelperFnFromIdentifier(id), viaModule: importInfo.from};
}
private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile

View File

@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, absoluteFrom} from '../../src/ngtsc/file_system';
import {KnownDeclaration} from '../../src/ngtsc/reflection';
/**
* A list (`Array`) of partially ordered `T` items.
@ -132,6 +133,40 @@ export function resolveFileWithPostfixes(
return null;
}
/**
* Determine whether a function declaration corresponds with a TypeScript helper function, returning
* its kind if so or null if the declaration does not seem to correspond with such a helper.
*/
export function getTsHelperFnFromDeclaration(decl: ts.Declaration): KnownDeclaration|null {
if (!ts.isFunctionDeclaration(decl) && !ts.isVariableDeclaration(decl)) {
return null;
}
if (decl.name === undefined || !ts.isIdentifier(decl.name)) {
return null;
}
return getTsHelperFnFromIdentifier(decl.name);
}
/**
* Determine whether an identifier corresponds with a TypeScript helper function (based on its
* name), returning its kind if so or null if the identifier does not seem to correspond with such a
* helper.
*/
export function getTsHelperFnFromIdentifier(id: ts.Identifier): KnownDeclaration|null {
switch (stripDollarSuffix(id.text)) {
case '__assign':
return KnownDeclaration.TsHelperAssign;
case '__spread':
return KnownDeclaration.TsHelperSpread;
case '__spreadArrays':
return KnownDeclaration.TsHelperSpreadArrays;
default:
return null;
}
}
/**
* An identifier may become repeated when bundling multiple source files into a single bundle, so
* bundlers have a strategy of suffixing non-unique identifiers with a suffix like $2. This function