From 5fa7b8ba562ffce90cdd31116c173eb3eac1c161 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 7 Apr 2020 19:14:57 +0300 Subject: [PATCH] fix(ngcc): detect non-emitted, non-imported TypeScript helpers (#36418) When TypeScript downlevels ES2015+ code to ES5, it uses some helper functions to emulate some ES2015+ features, such as spread syntax. The TypeScript compiler can be configured to emit these helpers into the transpiled code (which is controlled by the `noEmitHelpers` option - false by default). It can also be configured to import these helpers from the `tslib` module (which is controlled by the `importHelpers` option - false by default). While most of the time the helpers will be either emitted or imported, it is possible that one configures their app to neither emit nor import them. In that case, the helpers could, for example, be made available on the global object. This is what `@nativescript/angular` v9.0.0-next-2019-11-12-155500-01 does. See, for example, [common.js][1]. Ngcc must be able to detect and statically evaluate these helpers. Previously, it was only able to detect emitted or imported helpers. This commit adds support for detecting these helpers if they are neither emitted nor imported. It does this by checking identifiers for which no declaration (either concrete or inline) can be found against a list of known TypeScript helper function names. [1]: https://unpkg.com/browse/@nativescript/angular@9.0.0-next-2019-11-12-155500-01/common.js PR Close #36418 --- .../compiler-cli/ngcc/src/host/esm5_host.ts | 18 ++++- .../ngcc/test/host/commonjs_host_spec.ts | 68 ++++++++++++++++ .../ngcc/test/host/esm5_host_spec.ts | 62 +++++++++++++++ .../ngcc/test/host/umd_host_spec.ts | 78 +++++++++++++++++++ 4 files changed, 225 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 67c7abb291..17fd2134c8 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, isNamedVariableDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; -import {getNameText, getTsHelperFnFromDeclaration, hasNameIdentifier} from '../utils'; +import {getNameText, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils'; import {Esm2015ReflectionHost, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement, ParamInfo} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; @@ -192,6 +192,22 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { const superDeclaration = super.getDeclarationOfIdentifier(id); + if (superDeclaration === null) { + const nonEmittedNorImportedTsHelperDeclaration = getTsHelperFnFromIdentifier(id); + if (nonEmittedNorImportedTsHelperDeclaration !== null) { + // No declaration could be found for this identifier and its name matches a known TS helper + // function. This can happen if a package is compiled with `noEmitHelpers: true` and + // `importHelpers: false` (the default). This is, for example, the case with + // `@nativescript/angular@9.0.0-next-2019-11-12-155500-01`. + return { + expression: id, + known: nonEmittedNorImportedTsHelperDeclaration, + node: null, + viaModule: null, + }; + } + } + if (superDeclaration === null || superDeclaration.node === null || superDeclaration.known !== null) { return superDeclaration; 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 421bab1b0d..b4a4231371 100644 --- a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -2000,6 +2000,74 @@ exports.ExternalModule = ExternalModule; testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); }); + + it('should recognize undeclared, unimported TypeScript helpers (by name)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + 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 = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + + const testForHelper = + (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = + getDeclaration(bundle.program, file.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed, undeclared, unimported TypeScript helpers (by name)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + 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 = + createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle)); + + const testForHelper = + (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = + getDeclaration(bundle.program, file.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); }); describe('getExportsOfModule()', () => { 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 8182a8807d..0f5190a3c2 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -2124,6 +2124,68 @@ runInEachFileSystem(() => { testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); }); + + it('should recognize undeclared, unimported TypeScript helpers (by name)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + 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 = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + + const testForHelper = (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = getDeclaration(bundle.program, file.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed, undeclared, unimported TypeScript helpers (by name)', () => { + const file: TestFile = { + name: _('/test.js'), + contents: ` + 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 = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle)); + + const testForHelper = (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = getDeclaration(bundle.program, file.name, varName, ts.isVariableDeclaration); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); }); describe('getExportsOfModule()', () => { 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 89319ab05f..5aea2a1f13 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -2101,6 +2101,84 @@ runInEachFileSystem(() => { testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread, 'tslib'); testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays, 'tslib'); }); + + it('should recognize undeclared, unimported TypeScript helpers (by name)', () => { + 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 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 = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0])!; + + const testForHelper = (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = getVariableDeclaration(factoryFn, varName); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays', KnownDeclaration.TsHelperSpreadArrays); + }); + + it('should recognize suffixed, undeclared, unimported TypeScript helpers (by name)', () => { + 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 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 = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const {factoryFn} = parseStatementForUmdModule( + getSourceFileOrError(bundle.program, file.name).statements[0])!; + + const testForHelper = (varName: string, helperName: string, knownAs: KnownDeclaration) => { + const node = getVariableDeclaration(factoryFn, varName); + const helperIdentifier = getIdentifierFromCallExpression(node); + const helperDeclaration = host.getDeclarationOfIdentifier(helperIdentifier); + + expect(helperDeclaration).toEqual({ + known: knownAs, + expression: helperIdentifier, + node: null, + viaModule: null, + }); + }; + + testForHelper('a', '__assign$1', KnownDeclaration.TsHelperAssign); + testForHelper('b', '__spread$2', KnownDeclaration.TsHelperSpread); + testForHelper('c', '__spreadArrays$3', KnownDeclaration.TsHelperSpreadArrays); + }); }); describe('getExportsOfModule()', () => {