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:

committed by
Miško Hevery

parent
14744f27c5
commit
bd6a39c364
@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {FactoryMap, isRelativePath, stripExtension} from '../src/utils';
|
||||
import * as ts from 'typescript';
|
||||
import {KnownDeclaration} from '../../src/ngtsc/reflection';
|
||||
import {FactoryMap, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, isRelativePath, stripExtension} from '../src/utils';
|
||||
|
||||
describe('FactoryMap', () => {
|
||||
it('should return an existing value', () => {
|
||||
@ -50,6 +52,121 @@ describe('FactoryMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTsHelperFnFromDeclaration()', () => {
|
||||
const createFunctionDeclaration = (fnName?: string) => ts.createFunctionDeclaration(
|
||||
undefined, undefined, undefined, fnName, undefined, [], undefined, undefined);
|
||||
const createVariableDeclaration = (varName: string) =>
|
||||
ts.createVariableDeclaration(varName, undefined, undefined);
|
||||
|
||||
it('should recognize the `__assign` helper as function declaration', () => {
|
||||
const decl1 = createFunctionDeclaration('__assign');
|
||||
const decl2 = createFunctionDeclaration('__assign$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
});
|
||||
|
||||
it('should recognize the `__assign` helper as variable declaration', () => {
|
||||
const decl1 = createVariableDeclaration('__assign');
|
||||
const decl2 = createVariableDeclaration('__assign$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
});
|
||||
|
||||
it('should recognize the `__spread` helper as function declaration', () => {
|
||||
const decl1 = createFunctionDeclaration('__spread');
|
||||
const decl2 = createFunctionDeclaration('__spread$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
});
|
||||
|
||||
it('should recognize the `__spread` helper as variable declaration', () => {
|
||||
const decl1 = createVariableDeclaration('__spread');
|
||||
const decl2 = createVariableDeclaration('__spread$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
});
|
||||
|
||||
it('should recognize the `__spreadArrays` helper as function declaration', () => {
|
||||
const decl1 = createFunctionDeclaration('__spreadArrays');
|
||||
const decl2 = createFunctionDeclaration('__spreadArrays$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
});
|
||||
|
||||
it('should recognize the `__spreadArrays` helper as variable declaration', () => {
|
||||
const decl1 = createVariableDeclaration('__spreadArrays');
|
||||
const decl2 = createVariableDeclaration('__spreadArrays$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
});
|
||||
|
||||
it('should return null for unrecognized helpers', () => {
|
||||
const decl1 = createFunctionDeclaration('__foo');
|
||||
const decl2 = createVariableDeclaration('spread');
|
||||
const decl3 = createFunctionDeclaration('spread$42');
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(decl1)).toBe(null);
|
||||
expect(getTsHelperFnFromDeclaration(decl2)).toBe(null);
|
||||
expect(getTsHelperFnFromDeclaration(decl3)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for unnamed declarations', () => {
|
||||
const unnamledDecl = createFunctionDeclaration(undefined);
|
||||
|
||||
expect(getTsHelperFnFromDeclaration(unnamledDecl)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for non-function/variable declarations', () => {
|
||||
const classDecl =
|
||||
ts.createClassDeclaration(undefined, undefined, '__assign', undefined, undefined, []);
|
||||
|
||||
expect(classDecl.name !.text).toBe('__assign');
|
||||
expect(getTsHelperFnFromDeclaration(classDecl)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTsHelperFnFromIdentifier()', () => {
|
||||
it('should recognize the `__assign` helper', () => {
|
||||
const id1 = ts.createIdentifier('__assign');
|
||||
const id2 = ts.createIdentifier('__assign$42');
|
||||
|
||||
expect(getTsHelperFnFromIdentifier(id1)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
expect(getTsHelperFnFromIdentifier(id2)).toBe(KnownDeclaration.TsHelperAssign);
|
||||
});
|
||||
|
||||
it('should recognize the `__spread` helper', () => {
|
||||
const id1 = ts.createIdentifier('__spread');
|
||||
const id2 = ts.createIdentifier('__spread$42');
|
||||
|
||||
expect(getTsHelperFnFromIdentifier(id1)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
expect(getTsHelperFnFromIdentifier(id2)).toBe(KnownDeclaration.TsHelperSpread);
|
||||
});
|
||||
|
||||
it('should recognize the `__spreadArrays` helper', () => {
|
||||
const id1 = ts.createIdentifier('__spreadArrays');
|
||||
const id2 = ts.createIdentifier('__spreadArrays$42');
|
||||
|
||||
expect(getTsHelperFnFromIdentifier(id1)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
expect(getTsHelperFnFromIdentifier(id2)).toBe(KnownDeclaration.TsHelperSpreadArrays);
|
||||
});
|
||||
|
||||
it('should return null for unrecognized helpers', () => {
|
||||
const id1 = ts.createIdentifier('__foo');
|
||||
const id2 = ts.createIdentifier('spread');
|
||||
const id3 = ts.createIdentifier('spread$42');
|
||||
|
||||
expect(getTsHelperFnFromIdentifier(id1)).toBe(null);
|
||||
expect(getTsHelperFnFromIdentifier(id2)).toBe(null);
|
||||
expect(getTsHelperFnFromIdentifier(id3)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRelativePath()', () => {
|
||||
it('should return true for relative paths', () => {
|
||||
expect(isRelativePath('.')).toBe(true);
|
||||
|
Reference in New Issue
Block a user