diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts index 18dae5b3ee..762a77d0a6 100644 --- a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -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 diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 725cf4e2ae..6ffbe33952 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -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. diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts index 5fea53102d..1486dedea4 100644 --- a/packages/compiler-cli/ngcc/src/host/umd_host.ts +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -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 diff --git a/packages/compiler-cli/ngcc/src/utils.ts b/packages/compiler-cli/ngcc/src/utils.ts index 65644083d8..fc633164f8 100644 --- a/packages/compiler-cli/ngcc/src/utils.ts +++ b/packages/compiler-cli/ngcc/src/utils.ts @@ -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 diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts index ae02f2e410..9eb7ed5935 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, CtorParameter, InlineDeclaration, KnownDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; @@ -1660,6 +1660,32 @@ exports.ExternalModule = ExternalModule; }); describe('getDeclarationOfIdentifier', () => { + // Helpers + const createTestForTsHelper = + (program: ts.Program, host: CommonJsReflectionHost, srcFile: TestFile, + getHelperDeclaration: (name: string) => ts.Declaration) => + (varName: string, helperName: string, knownAs: KnownDeclaration, + viaModule: string | null = null) => { + const node = + getDeclaration(program, srcFile.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + node: getHelperDeclaration(helperName), viaModule, + }); + }; + + const getIdentifierFromCallExpression = (decl: ts.VariableDeclaration) => { + if (decl.initializer !== undefined && ts.isCallExpression(decl.initializer)) { + const expr = decl.initializer.expression; + if (ts.isIdentifier(expr)) return expr; + if (ts.isPropertyAccessExpression(expr)) return expr.name; + } + throw new Error(`Unable to extract identifier from declaration '${decl.getText()}'.`); + }; + it('should return the declaration of a locally defined identifier', () => { loadTestFiles([SOME_DIRECTIVE_FILE]); const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); @@ -1819,6 +1845,150 @@ exports.ExternalModule = ExternalModule; expect(decl.viaModule).toEqual('sub_module'); expect(decl.node).toBe(expectedDeclaration); }); + + it('should recognize TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + function __assign(t, ...sources) { /* ... */ } + function __spread(...args) { /* ... */ } + function __spreadArrays(...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + function __assign$1(t, ...sources) { /* ... */ } + function __spread$2(...args) { /* ... */ } + function __spreadArrays$3(...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + var __assign = (this && this.__assign) || function (t, ...sources) { /* ... */ } + var __spread = (this && this.__spread) || function (...args) { /* ... */ } + var __spreadArrays = (this && this.__spreadArrays) || function (...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isVariableDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + var __assign$1 = (this && this.__assign$1) || function (t, ...sources) { /* ... */ } + var __spread$2 = (this && this.__spread$2) || function (...args) { /* ... */ } + var __spreadArrays$3 = (this && this.__spreadArrays$3) || function (...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isVariableDeclaration)); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize imported TypeScript helpers', () => { + const files: TestFile[] = [ + { + name: _('/test.js'), + contents: ` + var tslib_1 = require('tslib'); + + var a = tslib_1.__assign({foo: 'bar'}, {baz: 'qux'}); + var b = tslib_1.__spread(['foo', 'bar'], ['baz', 'qux']); + var c = tslib_1.__spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }, + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + `, + }, + ]; + loadTestFiles(files); + + const [testFile, tslibFile] = files; + const bundle = makeTestBundleProgram(testFile.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + const tslibSourceFile = getSourceFileOrError(bundle.program, tslibFile.name); + + const testForHelper = + createTestForTsHelper(bundle.program, host, testFile, () => tslibSourceFile); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign, 'tslib'); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); + }); }); describe('getExportsOfModule()', () => { @@ -1918,6 +2088,31 @@ exports.ExternalModule = ExternalModule; expect(decl.node).toBeNull(); expect(decl.expression).toBeDefined(); }); + + it('should recognize declarations of known TypeScript helpers', () => { + const tslib = { + name: _('/tslib.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + export declare function __unknownHelper(...args: any[]): any; + `, + }; + loadTestFiles([tslib]); + const bundle = makeTestBundleProgram(tslib.name); + const host = new CommonJsReflectionHost(new MockLogger(), false, bundle); + const sf = getSourceFileOrError(bundle.program, tslib.name); + const exportDeclarations = host.getExportsOfModule(sf) !; + + expect([...exportDeclarations].map(([exportName, {known}]) => [exportName, known])) + .toEqual([ + ['__assign', KnownDeclaration.TsHelperAssign], + ['__spread', KnownDeclaration.TsHelperSpread], + ['__spreadArrays', KnownDeclaration.TsHelperSpreadArrays], + ['__unknownHelper', null], + ]); + }); }); describe('getClassSymbol()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 128875fb29..6479086607 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, Decorator, Import, TsHelperFn, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, CtorParameter, Decorator, KnownDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; @@ -1689,224 +1689,6 @@ runInEachFileSystem(() => { expect(quxDef.parameters[0].name).toEqual('x'); expect(quxDef.parameters[0].initializer).toBe(null); }); - - it('should recognize TypeScript __spread helper function declaration', () => { - const file: TestFile = { - name: _('/declaration.d.ts'), - contents: `export declare function __spread(...args: any[][]): any[];`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__spread', isNamedFunctionDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Spread); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spread helper function implementation', () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __spread = (this && this.__spread) || function () { - for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); - return ar; - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__spread', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Spread); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spread helper function implementation when suffixed', - () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __spread$2 = (this && this.__spread$2) || function () { - for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); - return ar; - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__spread$2', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Spread); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spreadArrays helper function declaration', () => { - const file: TestFile = { - name: _('/declaration.d.ts'), - contents: `export declare function __spreadArrays(...args: any[][]): any[];`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = getDeclaration( - bundle.program, file.name, '__spreadArrays', isNamedFunctionDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.SpreadArrays); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spreadArrays helper function implementation', () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __spreadArrays = (this && this.__spreadArrays) || function () { - for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; - for (var r = Array(s), k = 0, i = 0; i < il; i++) - for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) - r[k] = a[j]; - return r; - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__spreadArrays', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.SpreadArrays); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __spreadArrays helper function implementation when suffixed', - () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __spreadArrays$2 = (this && this.__spreadArrays$2) || function () { - for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; - for (var r = Array(s), k = 0, i = 0; i < il; i++) - for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) - r[k] = a[j]; - return r; - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = getDeclaration( - bundle.program, file.name, '__spreadArrays$2', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.SpreadArrays); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __assign helper function declaration', () => { - const file: TestFile = { - name: _('/declaration.d.ts'), - contents: `export declare function __assign(...args: object[]): object;`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__assign', isNamedFunctionDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Assign); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __assign helper function implementation', () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__assign', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Assign); - expect(definition.parameters.length).toEqual(0); - }); - - it('should recognize TypeScript __assign helper function implementation when suffixed', - () => { - const file: TestFile = { - name: _('/implementation.js'), - contents: ` - var __assign$2 = (this && this.__assign$2) || function () { - __assign$2 = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign$2.apply(this, arguments); - };`, - }; - loadTestFiles([file]); - const bundle = makeTestBundleProgram(file.name); - const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); - - const node = - getDeclaration(bundle.program, file.name, '__assign$2', ts.isVariableDeclaration) !; - - const definition = host.getDefinitionOfFunction(node) !; - expect(definition.node).toBe(node); - expect(definition.body).toBeNull(); - expect(definition.helper).toBe(TsHelperFn.Assign); - expect(definition.parameters.length).toEqual(0); - }); }); describe('getImportOfIdentifier()', () => { @@ -1945,6 +1727,32 @@ runInEachFileSystem(() => { }); describe('getDeclarationOfIdentifier()', () => { + // Helpers + const createTestForTsHelper = + (program: ts.Program, host: Esm5ReflectionHost, srcFile: TestFile, + getHelperDeclaration: (name: string) => ts.Declaration) => + (varName: string, helperName: string, knownAs: KnownDeclaration, + viaModule: string | null = null) => { + const node = + getDeclaration(program, srcFile.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + node: getHelperDeclaration(helperName), viaModule, + }); + }; + + const getIdentifierFromCallExpression = (decl: ts.VariableDeclaration) => { + if (decl.initializer !== undefined && ts.isCallExpression(decl.initializer)) { + const expr = decl.initializer.expression; + if (ts.isIdentifier(expr)) return expr; + if (ts.isPropertyAccessExpression(expr)) return expr.name; + } + throw new Error(`Unable to extract identifier from declaration '${decl.getText()}'.`); + }; + it('should return the declaration of a locally defined identifier', () => { loadTestFiles([SOME_DIRECTIVE_FILE]); const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); @@ -2117,6 +1925,188 @@ runInEachFileSystem(() => { const actualDeclaration = host.getDeclarationOfIdentifier(identifier) !; expect(actualDeclaration.node !.getText()).toBe(expectedDeclaration.getText()); }); + + it('should recognize TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + function __assign(t, ...sources) { /* ... */ } + function __spread(...args) { /* ... */ } + function __spreadArrays(...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + function __assign$1(t, ...sources) { /* ... */ } + function __spread$2(...args) { /* ... */ } + function __spreadArrays$3(...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + var __assign = (this && this.__assign) || function (t, ...sources) { /* ... */ } + var __spread = (this && this.__spread) || function (...args) { /* ... */ } + var __spreadArrays = (this && this.__spreadArrays) || function (...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isVariableDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + var __assign$1 = (this && this.__assign$1) || function (t, ...sources) { /* ... */ } + var __spread$2 = (this && this.__spread$2) || function (...args) { /* ... */ } + var __spreadArrays$3 = (this && this.__spreadArrays$3) || function (...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, file, + helperName => + getDeclaration(bundle.program, file.name, helperName, ts.isVariableDeclaration)); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize imported TypeScript helpers (named imports)', () => { + const files: TestFile[] = [ + { + name: _('/test.js'), + contents: ` + import {__assign, __spread, __spreadArrays} from 'tslib'; + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }, + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + `, + }, + ]; + loadTestFiles(files); + + const [testFile, tslibFile] = files; + const bundle = makeTestBundleProgram(testFile.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, testFile, + helperName => getDeclaration( + bundle.program, tslibFile.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign, 'tslib'); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); + }); + + it('should recognize imported TypeScript helpers (star import)', () => { + const files: TestFile[] = [ + { + name: _('/test.js'), + contents: ` + import * as tslib_1 from 'tslib'; + + var a = tslib_1.__assign({foo: 'bar'}, {baz: 'qux'}); + var b = tslib_1.__spread(['foo', 'bar'], ['baz', 'qux']); + var c = tslib_1.__spreadArrays(['foo', 'bar'], ['baz', 'qux']); + `, + }, + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + `, + }, + ]; + loadTestFiles(files); + + const [testFile, tslibFile] = files; + const bundle = makeTestBundleProgram(testFile.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + + const testForHelper = createTestForTsHelper( + bundle.program, host, testFile, + helperName => getDeclaration( + bundle.program, tslibFile.name, helperName, ts.isFunctionDeclaration)); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign, 'tslib'); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); + }); }); describe('getExportsOfModule()', () => { @@ -2159,6 +2149,31 @@ runInEachFileSystem(() => { ], ]); }); + + it('should recognize declarations of known TypeScript helpers', () => { + const tslib = { + name: _('/tslib.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + export declare function __unknownHelper(...args: any[]): any; + `, + }; + loadTestFiles([tslib]); + const bundle = makeTestBundleProgram(tslib.name); + const host = new Esm5ReflectionHost(new MockLogger(), false, bundle); + const sf = getSourceFileOrError(bundle.program, tslib.name); + const exportDeclarations = host.getExportsOfModule(sf) !; + + expect([...exportDeclarations].map(([exportName, {known}]) => [exportName, known])) + .toEqual([ + ['__assign', KnownDeclaration.TsHelperAssign], + ['__spread', KnownDeclaration.TsHelperSpread], + ['__spreadArrays', KnownDeclaration.TsHelperSpreadArrays], + ['__unknownHelper', null], + ]); + }); }); describe('getClassSymbol()', () => { diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 213f3a008f..842cded12f 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -10,11 +10,11 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, CtorParameter, Import, InlineDeclaration, KnownDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; import {getIifeBody} from '../../src/host/esm5_host'; -import {UmdReflectionHost} from '../../src/host/umd_host'; +import {UmdReflectionHost, parseStatementForUmdModule} from '../../src/host/umd_host'; import {MockLogger} from '../helpers/mock_logger'; import {getRootFiles, makeTestBundleProgram} from '../helpers/utils'; @@ -1827,6 +1827,41 @@ runInEachFileSystem(() => { }); describe('getDeclarationOfIdentifier', () => { + // Helpers + const createTestForTsHelper = + (host: UmdReflectionHost, factoryFn: ts.FunctionExpression, + getHelperDeclaration: (factoryFn: ts.FunctionExpression, name: string) => + ts.Declaration) => + (varName: string, helperName: string, knownAs: KnownDeclaration, + viaModule: string | null = null) => { + const node = getVariableDeclaration(factoryFn, varName); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + node: getHelperDeclaration(factoryFn, helperName), viaModule, + }); + }; + + const getFunctionDeclaration = (factoryFn: ts.FunctionExpression, name: string) => + factoryFn.body.statements.filter(ts.isFunctionDeclaration) + .find(decl => (decl.name !== undefined) && (decl.name.text === name)) !; + + const getIdentifierFromCallExpression = (decl: ts.VariableDeclaration) => { + if (decl.initializer !== undefined && ts.isCallExpression(decl.initializer)) { + const expr = decl.initializer.expression; + if (ts.isIdentifier(expr)) return expr; + if (ts.isPropertyAccessExpression(expr)) return expr.name; + } + throw new Error(`Unable to extract identifier from declaration '${decl.getText()}'.`); + }; + + const getVariableDeclaration = (factoryFn: ts.FunctionExpression, name: string) => + factoryFn.body.statements.filter(ts.isVariableStatement) + .map(stmt => stmt.declarationList.declarations[0]) + .find(decl => ts.isIdentifier(decl.name) && (decl.name.text === name)) !; + it('should return the declaration of a locally defined identifier', () => { loadTestFiles([SOME_DIRECTIVE_FILE]); const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); @@ -1937,6 +1972,175 @@ runInEachFileSystem(() => { expect(decl.viaModule).toEqual('sub_module'); expect(decl.node).toBe(expectedDeclaration); }); + + it('should recognize TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('test', ['exports'], factory) : + (factory(global.test)); + }(this, (function (exports) { 'use strict'; + function __assign(t, ...sources) { /* ... */ } + function __spread(...args) { /* ... */ } + function __spreadArrays(...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + }))); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0]) !; + + const testForHelper = createTestForTsHelper(host, factoryFn, getFunctionDeclaration); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as function declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('test', ['exports'], factory) : + (factory(global.test)); + }(this, (function (exports) { 'use strict'; + function __assign$1(t, ...sources) { /* ... */ } + function __spread$2(...args) { /* ... */ } + function __spreadArrays$3(...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + }))); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0]) !; + + const testForHelper = createTestForTsHelper(host, factoryFn, getFunctionDeclaration); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('test', ['exports'], factory) : + (factory(global.test)); + }(this, (function (exports) { 'use strict'; + var __assign = (this && this.__assign) || function (t, ...sources) { /* ... */ } + var __spread = (this && this.__spread) || function (...args) { /* ... */ } + var __spreadArrays = (this && this.__spreadArrays) || function (...args) { /* ... */ } + + var a = __assign({foo: 'bar'}, {baz: 'qux'}); + var b = __spread(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays(['foo', 'bar'], ['baz', 'qux']); + }))); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0]) !; + + const testForHelper = createTestForTsHelper(host, factoryFn, getVariableDeclaration); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed TypeScript helpers (as variable declarations)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('test', ['exports'], factory) : + (factory(global.test)); + }(this, (function (exports) { 'use strict'; + var __assign$1 = (this && this.__assign$1) || function (t, ...sources) { /* ... */ } + var __spread$2 = (this && this.__spread$2) || function (...args) { /* ... */ } + var __spreadArrays$3 = (this && this.__spreadArrays$3) || function (...args) { /* ... */ } + + var a = __assign$1({foo: 'bar'}, {baz: 'qux'}); + var b = __spread$2(['foo', 'bar'], ['baz', 'qux']); + var c = __spreadArrays$3(['foo', 'bar'], ['baz', 'qux']); + }))); + `, + }; + loadTestFiles([file]); + const bundle = makeTestBundleProgram(file.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0]) !; + + const testForHelper = createTestForTsHelper(host, factoryFn, getVariableDeclaration); + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize imported TypeScript helpers', () => { + const files: TestFile[] = [ + { + name: _('/test.js'), + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tslib')) : + typeof define === 'function' && define.amd ? define('test', ['exports', 'tslib'], factory) : + (factory(global.test, global.tslib)); + }(this, (function (exports, tslib_1) { 'use strict'; + var a = tslib_1.__assign({foo: 'bar'}, {baz: 'qux'}); + var b = tslib_1.__spread(['foo', 'bar'], ['baz', 'qux']); + var c = tslib_1.__spreadArrays(['foo', 'bar'], ['baz', 'qux']); + }))); + `, + }, + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + `, + }, + ]; + loadTestFiles(files); + + const [testFile, tslibFile] = files; + const bundle = makeTestBundleProgram(testFile.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, testFile.name).statements[0]) !; + const tslibSourceFile = getSourceFileOrError(bundle.program, tslibFile.name); + + const testForHelper = createTestForTsHelper(host, factoryFn, () => tslibSourceFile); + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign, 'tslib'); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); + }); }); describe('getExportsOfModule()', () => { @@ -2038,6 +2242,31 @@ runInEachFileSystem(() => { expect(decl.node).toBeNull(); expect(decl.expression).toBeDefined(); }); + + it('should recognize declarations of known TypeScript helpers', () => { + const tslib = { + name: _('/tslib.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + export declare function __unknownHelper(...args: any[]): any; + `, + }; + loadTestFiles([tslib]); + const bundle = makeTestBundleProgram(tslib.name); + const host = new UmdReflectionHost(new MockLogger(), false, bundle); + const sf = getSourceFileOrError(bundle.program, tslib.name); + const exportDeclarations = host.getExportsOfModule(sf) !; + + expect([...exportDeclarations].map(([exportName, {known}]) => [exportName, known])) + .toEqual([ + ['__assign', KnownDeclaration.TsHelperAssign], + ['__spread', KnownDeclaration.TsHelperSpread], + ['__spreadArrays', KnownDeclaration.TsHelperSpreadArrays], + ['__unknownHelper', null], + ]); + }); }); describe('getClassSymbol()', () => { diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 1637f491e3..721519c350 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -147,46 +147,79 @@ runInEachFileSystem(() => { }); ['esm5', 'esm2015'].forEach(target => { - it(`should be able to process spread operator inside objects for ${target} format`, () => { - compileIntoApf( - 'test-package', { - '/index.ts': ` - import {Directive, Input, NgModule} from '@angular/core'; + it(`should be able to process spread operator inside objects for ${target} format (imported helpers)`, + () => { + compileIntoApf( + 'test-package', { + '/index.ts': ` + import {Directive, Input, NgModule} from '@angular/core'; - const a = { '[class.a]': 'true' }; - const b = { '[class.b]': 'true' }; + const a = { '[class.a]': 'true' }; + const b = { '[class.b]': 'true' }; - @Directive({ - selector: '[foo]', - host: {...a, ...b, '[class.c]': 'false'} - }) - export class FooDirective {} + @Directive({ + selector: '[foo]', + host: {...a, ...b, '[class.c]': 'false'} + }) + export class FooDirective {} - @NgModule({ - declarations: [FooDirective], - }) - export class FooModule {} - `, - }, - {importHelpers: true}); + @NgModule({ + declarations: [FooDirective], + }) + export class FooModule {} + `, + }, + {importHelpers: true, noEmitHelpers: true}); - // TODO: add test with import helpers disabled. This currently won't work because - // inlined TS helper functions are not detected. For more details, see PR: - // https://github.com/angular/angular/pull/34169 - fs.writeFile( - _('/node_modules/tslib/index.d.ts'), - `export declare function __assign(...args: object[]): object;`); + fs.writeFile( + _('/node_modules/tslib/index.d.ts'), + `export declare function __assign(...args: object[]): object;`); - mainNgcc({ - basePath: '/node_modules', - targetEntryPointPath: 'test-package', - propertiesToConsider: [target], - }); + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: [target], + }); - const jsContents = fs.readFile(_(`/node_modules/test-package/${target}/src/index.js`)) - .replace(/\s+/g, ' '); - expect(jsContents).toContain('ngcc0.ɵɵclassProp("a", true)("b", true)("c", false)'); - }); + const jsContents = fs.readFile(_(`/node_modules/test-package/${target}/src/index.js`)) + .replace(/\s+/g, ' '); + expect(jsContents).toContain('ngcc0.ɵɵclassProp("a", true)("b", true)("c", false)'); + }); + + it(`should be able to process emitted spread operator inside objects for ${target} format (emitted helpers)`, + () => { + compileIntoApf( + 'test-package', { + '/index.ts': ` + import {Directive, Input, NgModule} from '@angular/core'; + + const a = { '[class.a]': 'true' }; + const b = { '[class.b]': 'true' }; + + @Directive({ + selector: '[foo]', + host: {...a, ...b, '[class.c]': 'false'} + }) + export class FooDirective {} + + @NgModule({ + declarations: [FooDirective], + }) + export class FooModule {} + `, + }, + {importHelpers: false, noEmitHelpers: false}); + + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: 'test-package', + propertiesToConsider: [target], + }); + + const jsContents = fs.readFile(_(`/node_modules/test-package/${target}/src/index.js`)) + .replace(/\s+/g, ' '); + expect(jsContents).toContain('ngcc0.ɵɵclassProp("a", true)("b", true)("c", false)'); + }); }); it('should not add `const` in ES5 generated code', () => { diff --git a/packages/compiler-cli/ngcc/test/utils_spec.ts b/packages/compiler-cli/ngcc/test/utils_spec.ts index 45d69b02ba..e6c292df72 100644 --- a/packages/compiler-cli/ngcc/test/utils_spec.ts +++ b/packages/compiler-cli/ngcc/test/utils_spec.ts @@ -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); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index 198a5f93fa..3929561b89 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -19,7 +19,6 @@ import {DynamicValue} from './dynamic'; import {ForeignFunctionResolver} from './interface'; import {resolveKnownDeclaration} from './known_declaration'; import {EnumValue, KnownFn, ResolvedModule, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result'; -import {evaluateTsHelperInline} from './ts_helpers'; @@ -333,6 +332,10 @@ export class StaticInterpreter { } return new ResolvedModule(declarations, decl => { + if (decl.known !== null) { + return resolveKnownDeclaration(decl.known); + } + const declContext = { ...context, ...joinModuleContext(context, node, decl), }; @@ -417,12 +420,6 @@ export class StaticInterpreter { return DynamicValue.fromInvalidExpressionType(node.expression, lhs); } - // If the function corresponds with a tslib helper function, evaluate it with custom logic. - if (fn.helper !== null) { - const args = this.evaluateFunctionArguments(node, context); - return evaluateTsHelperInline(fn.helper, node, args); - } - if (!isFunctionOrMethodReference(lhs)) { return DynamicValue.fromInvalidExpressionType(node.expression, lhs); } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts index 0ba3484afe..2456e672b2 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts @@ -10,19 +10,31 @@ import {KnownDeclaration} from '../../reflection/src/host'; import {ObjectAssignBuiltinFn} from './builtin'; import {ResolvedValue} from './result'; +import {AssignHelperFn, SpreadHelperFn} from './ts_helpers'; -/** Resolved value for the JavaScript global `Object` declaration .*/ +/** Resolved value for the JavaScript global `Object` declaration. */ export const jsGlobalObjectValue = new Map([['assign', new ObjectAssignBuiltinFn()]]); +/** Resolved value for the `__assign()` TypeScript helper declaration. */ +const assignTsHelperFn = new AssignHelperFn(); + +/** Resolved value for the `__spread()` and `__spreadArrays()` TypeScript helper declarations. */ +const spreadTsHelperFn = new SpreadHelperFn(); + /** * Resolves the specified known declaration to a resolved value. For example, * the known JavaScript global `Object` will resolve to a `Map` that provides the - * `assign` method with a builtin function. This enables evaluation of `Object.assign`. + * `assign` method with a built-in function. This enables evaluation of `Object.assign`. */ export function resolveKnownDeclaration(decl: KnownDeclaration): ResolvedValue { switch (decl) { case KnownDeclaration.JsGlobalObject: return jsGlobalObjectValue; + case KnownDeclaration.TsHelperAssign: + return assignTsHelperFn; + case KnownDeclaration.TsHelperSpread: + case KnownDeclaration.TsHelperSpreadArrays: + return spreadTsHelperFn; default: throw new Error(`Cannot resolve known declaration. Received: ${KnownDeclaration[decl]}.`); } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/ts_helpers.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/ts_helpers.ts index ad515b5977..469755db04 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/ts_helpers.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/ts_helpers.ts @@ -8,44 +8,30 @@ import * as ts from 'typescript'; -import {TsHelperFn} from '../../reflection'; - import {ObjectAssignBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; -import {ResolvedValue, ResolvedValueArray} from './result'; +import {KnownFn, ResolvedValueArray} from './result'; -/** - * Instance of the known `Object.assign` built-in function. Used for evaluating - * the `__assign` TypeScript helper. - */ -const objectAssignBuiltinFn = new ObjectAssignBuiltinFn(); +// Use the same implementation we use for `Object.assign()`. Semantically these functions are the +// same, so they can also share the same evaluation code. +export class AssignHelperFn extends ObjectAssignBuiltinFn {} -export function evaluateTsHelperInline( - helper: TsHelperFn, node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue { - switch (helper) { - case TsHelperFn.Assign: - // Use the same implementation we use for `Object.assign`. Semantically these - // functions are the same, so they can also share the same evaluation code. - return objectAssignBuiltinFn.evaluate(node, args); - case TsHelperFn.Spread: - case TsHelperFn.SpreadArrays: - return evaluateTsSpreadHelper(node, args); - default: - throw new Error(`Cannot evaluate TypeScript helper function: ${TsHelperFn[helper]}`); - } -} +// Used for both `__spread()` and `__spreadArrays()` TypeScript helper functions. +export class SpreadHelperFn extends KnownFn { + evaluate(node: ts.Node, args: ResolvedValueArray): ResolvedValueArray { + const result: ResolvedValueArray = []; -function evaluateTsSpreadHelper(node: ts.Node, args: ResolvedValueArray): ResolvedValueArray { - const result: ResolvedValueArray = []; - for (const arg of args) { - if (arg instanceof DynamicValue) { - result.push(DynamicValue.fromDynamicInput(node, arg)); - } else if (Array.isArray(arg)) { - result.push(...arg); - } else { - result.push(arg); + for (const arg of args) { + if (arg instanceof DynamicValue) { + result.push(DynamicValue.fromDynamicInput(node, arg)); + } else if (Array.isArray(arg)) { + result.push(...arg); + } else { + result.push(arg); + } } + + return result; } - return result; } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index 68495ddb08..a64fe61a51 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -11,11 +11,11 @@ import {absoluteFrom, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {Reference} from '../../imports'; import {DependencyTracker} from '../../incremental/api'; -import {FunctionDefinition, TsHelperFn, TypeScriptReflectionHost} from '../../reflection'; +import {Declaration, KnownDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {DynamicValue} from '../src/dynamic'; import {PartialEvaluator} from '../src/interface'; -import {EnumValue} from '../src/result'; +import {EnumValue, ResolvedValue} from '../src/result'; import {evaluate, firstArgFfr, makeEvaluator, makeExpression, owningModuleOf} from './utils'; @@ -538,69 +538,209 @@ runInEachFileSystem(() => { expect((value.node as ts.CallExpression).expression.getText()).toBe('foo'); }); - it('should evaluate TypeScript __spread helper', () => { - const {checker, expression} = makeExpression( - ` - import * as tslib from 'tslib'; - const a = [1]; - const b = [2, 3]; - `, - 'tslib.__spread(a, b)', [ - { - name: _('/node_modules/tslib/index.d.ts'), - contents: ` - export declare function __spread(...args: any[][]): any[]; - ` - }, - ]); - const reflectionHost = new TsLibAwareReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker, null); - const value = evaluator.evaluate(expression); - expect(value).toEqual([1, 2, 3]); + describe('(with imported TypeScript helpers)', () => { + // Helpers + const evaluateExpression = (code: string, expr: string) => { + const {checker, expression} = makeExpression(code, expr, [ + { + name: _('/node_modules/tslib/index.d.ts'), + contents: ` + export declare function __assign(t: any, ...sources: any[]): any; + export declare function __spread(...args: any[][]): any[]; + export declare function __spreadArrays(...args: any[][]): any[]; + `, + }, + ]); + + const reflectionHost = new TsLibAwareReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker, null); + + return evaluator.evaluate(expression) as T; + }; + + it('should evaluate `__assign()` (named import)', () => { + const map: Map = evaluateExpression( + ` + import {__assign} from 'tslib'; + const a = {a: true}; + const b = {b: true}; + `, + '__assign(a, b)'); + + expect([...map]).toEqual([ + ['a', true], + ['b', true], + ]); + }); + + it('should evaluate `__assign()` (star import)', () => { + const map: Map = evaluateExpression( + ` + import * as tslib from 'tslib'; + const a = {a: true}; + const b = {b: true}; + `, + 'tslib.__assign(a, b)'); + + expect([...map]).toEqual([ + ['a', true], + ['b', true], + ]); + }); + + it('should evaluate `__spread()` (named import)', () => { + const arr: number[] = evaluateExpression( + ` + import {__spread} from 'tslib'; + const a = [1]; + const b = [2, 3]; + `, + '__spread(a, b)'); + + expect(arr).toEqual([1, 2, 3]); + }); + + it('should evaluate `__spread()` (star import)', () => { + const arr: number[] = evaluateExpression( + ` + import * as tslib from 'tslib'; + const a = [1]; + const b = [2, 3]; + `, + 'tslib.__spread(a, b)'); + + expect(arr).toEqual([1, 2, 3]); + }); + + it('should evaluate `__spreadArrays()` (named import)', () => { + const arr: number[] = evaluateExpression( + ` + import {__spreadArrays} from 'tslib'; + const a = [4]; + const b = [5, 6]; + `, + '__spreadArrays(a, b)'); + + expect(arr).toEqual([4, 5, 6]); + }); + + it('should evaluate `__spreadArrays()` (star import)', () => { + const arr: number[] = evaluateExpression( + ` + import * as tslib from 'tslib'; + const a = [4]; + const b = [5, 6]; + `, + 'tslib.__spreadArrays(a, b)'); + + expect(arr).toEqual([4, 5, 6]); + }); }); - it('should evaluate TypeScript __spreadArrays helper', () => { - const {checker, expression} = makeExpression( - ` - import * as tslib from 'tslib'; - const a = [1]; - const b = [2, 3]; - `, - 'tslib.__spreadArrays(a, b)', [ - { - name: _('/node_modules/tslib/index.d.ts'), - contents: ` - export declare function __spreadArrays(...args: any[][]): any[]; - ` - }, - ]); - const reflectionHost = new TsLibAwareReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker, null); - const value = evaluator.evaluate(expression); - expect(value).toEqual([1, 2, 3]); + describe('(with emitted TypeScript helpers as functions)', () => { + // Helpers + const evaluateExpression = (code: string, expr: string) => { + const helpers = ` + function __assign(t, ...sources) { /* ... */ } + function __spread(...args) { /* ... */ } + function __spreadArrays(...args) { /* ... */ } + `; + const {checker, expression} = makeExpression(helpers + code, expr); + + const reflectionHost = new TsLibAwareReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker, null); + + return evaluator.evaluate(expression) as T; + }; + + it('should evaluate `__assign()`', () => { + const map: Map = evaluateExpression( + ` + const a = {a: true}; + const b = {b: true}; + `, + '__assign(a, b)'); + + expect([...map]).toEqual([ + ['a', true], + ['b', true], + ]); + }); + + it('should evaluate `__spread()`', () => { + const arr: number[] = evaluateExpression( + ` + const a = [1]; + const b = [2, 3]; + `, + '__spread(a, b)'); + + expect(arr).toEqual([1, 2, 3]); + }); + + it('should evaluate `__spreadArrays()`', () => { + const arr: number[] = evaluateExpression( + ` + const a = [4]; + const b = [5, 6]; + `, + '__spreadArrays(a, b)'); + + expect(arr).toEqual([4, 5, 6]); + }); }); - it('should evaluate TypeScript __assign helper', () => { - const {checker, expression} = makeExpression( - ` - import * as tslib from 'tslib'; - const a = {a: true}; - const b = {b: true}; - `, - 'tslib.__assign(a, b)', [ - { - name: _('/node_modules/tslib/index.d.ts'), - contents: ` - export declare function __assign(...args: object[]): object; - ` - }, - ]); - const reflectionHost = new TsLibAwareReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker, null); - const map = evaluator.evaluate(expression) as Map; - const obj: {[key: string]: boolean} = {}; - map.forEach((value, key) => obj[key] = value); - expect(obj).toEqual({a: true, b: true}); + describe('(with emitted TypeScript helpers as variables)', () => { + // Helpers + const evaluateExpression = (code: string, expr: string) => { + const helpers = ` + var __assign = (this && this.__assign) || function (t, ...sources) { /* ... */ } + var __spread = (this && this.__spread) || function (...args) { /* ... */ } + var __spreadArrays = (this && this.__spreadArrays) || function (...args) { /* ... */ } + `; + const {checker, expression} = makeExpression(helpers + code, expr); + + const reflectionHost = new TsLibAwareReflectionHost(checker); + const evaluator = new PartialEvaluator(reflectionHost, checker, null); + + return evaluator.evaluate(expression) as T; + }; + + it('should evaluate `__assign()`', () => { + const map: Map = evaluateExpression( + ` + const a = {a: true}; + const b = {b: true}; + `, + '__assign(a, b)'); + + expect([...map]).toEqual([ + ['a', true], + ['b', true], + ]); + }); + + it('should evaluate `__spread()`', () => { + const arr: number[] = evaluateExpression( + ` + const a = [1]; + const b = [2, 3]; + `, + '__spread(a, b)'); + + expect(arr).toEqual([1, 2, 3]); + }); + + it('should evaluate `__spreadArrays()`', () => { + const arr: number[] = evaluateExpression( + ` + const a = [4]; + const b = [5, 6]; + `, + '__spreadArrays(a, b)'); + + expect(arr).toEqual([4, 5, 6]); + }); }); describe('(visited file tracking)', () => { @@ -665,36 +805,52 @@ runInEachFileSystem(() => { }); /** - * Customizes the resolution of functions to recognize functions from tslib. Such functions are - * not handled specially in the default TypeScript host, as only ngcc's ES5 host will have special - * powers to recognize functions from tslib. + * Customizes the resolution of module exports and identifier declarations to recognize known + * helper functions from `tslib`. Such functions are not handled specially in the default + * TypeScript host, as only ngcc's ES5 hosts will have special powers to recognize such functions. */ class TsLibAwareReflectionHost extends TypeScriptReflectionHost { - getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { - if (ts.isFunctionDeclaration(node)) { - const helper = getTsHelperFn(node); - if (helper !== null) { - return { - node, - body: null, helper, - parameters: [], - }; - } + getExportsOfModule(node: ts.Node): Map|null { + const map = super.getExportsOfModule(node); + + if (map !== null) { + map.forEach(decl => decl.known = decl.known || (decl.node && getTsHelperFn(decl.node))); } - return super.getDefinitionOfFunction(node); + + return map; + } + + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + const superDeclaration = super.getDeclarationOfIdentifier(id); + + if (superDeclaration === null || superDeclaration.node === null) { + return superDeclaration; + } + + const tsHelperFn = getTsHelperFn(superDeclaration.node); + if (tsHelperFn !== null) { + return { + known: tsHelperFn, + node: id, + viaModule: null, + }; + } + + return superDeclaration; } } - function getTsHelperFn(node: ts.FunctionDeclaration): TsHelperFn|null { - const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text; + function getTsHelperFn(node: ts.Declaration): KnownDeclaration|null { + const id = (node as ts.Declaration & {name?: ts.Identifier}).name || null; + const name = id && id.text; switch (name) { case '__assign': - return TsHelperFn.Assign; + return KnownDeclaration.TsHelperAssign; case '__spread': - return TsHelperFn.Spread; + return KnownDeclaration.TsHelperSpread; case '__spreadArrays': - return TsHelperFn.SpreadArrays; + return KnownDeclaration.TsHelperSpreadArrays; default: return null; } diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index 431930550b..2857505d74 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -316,12 +316,6 @@ export interface FunctionDefinition { */ body: ts.Statement[]|null; - /** - * The type of tslib helper function, if the function is determined to represent a tslib helper - * function. Otherwise, this will be null. - */ - helper: TsHelperFn|null; - /** * Metadata regarding the function's parameters, including possible default value expressions. */ @@ -329,31 +323,28 @@ export interface FunctionDefinition { } /** - * Possible functions from TypeScript's helper library. - */ -export enum TsHelperFn { - /** - * Indicates the `__assign` function. - */ - Assign, - /** - * Indicates the `__spread` function. - */ - Spread, - /** - * Indicates the `__spreadArrays` function. - */ - SpreadArrays, -} - -/** - * Possible declarations which are known. + * Possible declarations of known values, such as built-in objects/functions or TypeScript helpers. */ export enum KnownDeclaration { /** * Indicates the JavaScript global `Object` class. */ JsGlobalObject, + + /** + * Indicates the `__assign` TypeScript helper function. + */ + TsHelperAssign, + + /** + * Indicates the `__spread` TypeScript helper function. + */ + TsHelperSpread, + + /** + * Indicates the `__spreadArrays` TypeScript helper function. + */ + TsHelperSpreadArrays, } /** diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index d1f8b424b5..c96ae69a09 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -164,7 +164,6 @@ export class TypeScriptReflectionHost implements ReflectionHost { return { node, body: node.body !== undefined ? Array.from(node.body.statements) : null, - helper: null, parameters: node.parameters.map(param => { const name = parameterName(param.name); const initializer = param.initializer || null; @@ -266,10 +265,8 @@ export class TypeScriptReflectionHost implements ReflectionHost { /** * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. - * - * @internal */ - private getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration + protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration |null { // If the symbol points to a ShorthandPropertyAssignment, resolve it. let valueDeclaration: ts.Declaration|undefined = undefined;