From 6b468f9b2e4b99cd9e784c2515484bce47147476 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 9 Jan 2020 20:37:02 +0100 Subject: [PATCH] fix(ngcc): libraries using spread in object literals cannot be processed (#34661) Consider a library that uses a shared constant for host bindings. e.g. ```ts export const BASE_BINDINGS= { '[class.mat-themed]': '_isThemed', } ---- @Directive({ host: {...BASE_BINDINGS, '(click)': '...'} }) export class Dir1 {} @Directive({ host: {...BASE_BINDINGS, '(click)': '...'} }) export class Dir2 {} ``` Previously when these components were shipped as part of the library to NPM, consumers were able to consume `Dir1` and `Dir2`. No errors showed up. Now with Ivy, when ngcc tries to process the library, an error will be thrown. The error is stating that the host bindings should be an object (which they obviously are). This happens because TypeScript transforms the object spread to individual `Object.assign` calls (for compatibility). The partial evaluator used by the `@Directive` annotation handler is unable to process this expression because there is no integrated support for `Object.assign`. In View Engine, this was not a problem because the `metadata.json` files from the library were used to compute the host bindings. Fixes #34659 PR Close #34661 --- .../module_with_providers_analyzer.ts | 2 +- .../ngcc/src/host/commonjs_host.ts | 8 +- .../ngcc/src/host/esm2015_host.ts | 37 ++++++++- .../compiler-cli/ngcc/src/host/esm5_host.ts | 3 + .../compiler-cli/ngcc/src/host/umd_host.ts | 8 +- .../ngcc/src/packages/entry_point_bundle.ts | 5 +- .../ngcc/test/host/esm5_host_spec.ts | 80 +++++++++++++++++++ .../ngcc/test/integration/ngcc_spec.ts | 43 ++++++++++ .../ngcc/test/integration/util.ts | 6 +- .../ngtsc/partial_evaluator/src/builtin.ts | 32 ++++++-- .../partial_evaluator/src/interpreter.ts | 10 ++- .../src/known_declaration.ts | 29 +++++++ .../src/ngtsc/partial_evaluator/src/result.ts | 4 +- .../ngtsc/partial_evaluator/src/ts_helpers.ts | 16 +++- .../partial_evaluator/test/evaluator_spec.ts | 25 ++++++ .../src/ngtsc/reflection/src/host.ts | 19 +++++ .../src/ngtsc/reflection/src/typescript.ts | 4 +- .../src/ngtsc/reflection/test/ts_host_spec.ts | 2 + 18 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index fc9f6a7cac..8c6ad1ee83 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -114,7 +114,7 @@ export class ModuleWithProvidersAnalyzer { `The referenced NgModule in ${fn.declaration.getText()} is not a named class declaration in the typings program; instead we get ${dtsNgModule.getText()}`); } - return {node: dtsNgModule, viaModule: null}; + return {node: dtsNgModule, known: null, viaModule: null}; } } diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts index 296a14af03..aa73065ee3 100644 --- a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -126,6 +126,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { name, declaration: { node: null, + known: null, expression: exportExpression, viaModule: null, }, @@ -159,9 +160,10 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { const reexports: ExportDeclaration[] = []; importedExports.forEach((decl, name) => { if (decl.node !== null) { - reexports.push({name, declaration: {node: decl.node, viaModule}}); + reexports.push({name, declaration: {node: decl.node, known: null, viaModule}}); } else { - reexports.push({name, declaration: {node: null, expression: decl.expression, viaModule}}); + reexports.push( + {name, declaration: {node: null, known: null, expression: decl.expression, viaModule}}); } }); return reexports; @@ -186,7 +188,7 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { } const viaModule = !importInfo.from.startsWith('.') ? importInfo.from : null; - return {node: importedFile, viaModule}; + return {node: importedFile, known: null, viaModule}; } private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 13b5d1e9af..fe3f03d9d5 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, TypeScriptReflectionHost, TypeValueReference, isDecoratorIdentifier, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, ConcreteDeclaration, CtorParameter, Declaration, Decorator, KnownDeclaration, TypeScriptReflectionHost, TypeValueReference, isDecoratorIdentifier, reflectObjectLiteral,} from '../../../src/ngtsc/reflection'; import {isWithinPackage} from '../analysis/util'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; @@ -353,6 +353,17 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } } + // If the identifier resolves to the global JavaScript `Object`, return a + // declaration that denotes it as the known `JsGlobalObject` declaration. + if (superDeclaration !== null && this.isJavaScriptObjectDeclaration(superDeclaration)) { + return { + known: KnownDeclaration.JsGlobalObject, + expression: id, + viaModule: null, + node: null, + }; + } + return superDeclaration; } @@ -1697,6 +1708,30 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const exportDecl = namespaceExports.get(expression.name.text) !; return {...exportDecl, viaModule: namespaceDecl.viaModule}; } + + /** Checks if the specified declaration resolves to the known JavaScript global `Object`. */ + protected isJavaScriptObjectDeclaration(decl: Declaration): boolean { + if (decl.node === null) { + return false; + } + const node = decl.node; + // The default TypeScript library types the global `Object` variable through + // a variable declaration with a type reference resolving to `ObjectConstructor`. + if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || + node.name.text !== 'Object' || node.type === undefined) { + return false; + } + const typeNode = node.type; + // If the variable declaration does not have a type resolving to `ObjectConstructor`, + // we cannot guarantee that the declaration resolves to the global `Object` variable. + if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName) || + typeNode.typeName.text !== 'ObjectConstructor') { + return false; + } + // Finally, check if the type definition for `Object` originates from a default library + // definition file. This requires default types to be enabled for the host program. + return this.src.program.isSourceFileDefaultLibrary(node.getSourceFile()); + } } ///////////// Exported Helpers ///////////// diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index e003921765..4307af7d23 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -14,6 +14,7 @@ import {getNameText, hasNameIdentifier, stripDollarSuffix} from '../utils'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignment, isAssignmentStatement} from './esm2015_host'; import {NgccClassSymbol} from './ngcc_host'; + /** * ESM5 packages contain ECMAScript IIFE functions that act like classes. For example: * @@ -655,6 +656,8 @@ function getTsHelperFn(node: ts.NamedDeclaration): TsHelperFn|null { null; switch (name) { + case '__assign': + return TsHelperFn.Assign; case '__spread': return TsHelperFn.Spread; case '__spreadArrays': diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts index 8cd3458db4..5fea53102d 100644 --- a/packages/compiler-cli/ngcc/src/host/umd_host.ts +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -140,6 +140,7 @@ export class UmdReflectionHost extends Esm5ReflectionHost { name, declaration: { node: null, + known: null, expression: exportExpression, viaModule: null, }, @@ -182,9 +183,10 @@ export class UmdReflectionHost extends Esm5ReflectionHost { const reexports: ExportDeclaration[] = []; importedExports.forEach((decl, name) => { if (decl.node !== null) { - reexports.push({name, declaration: {node: decl.node, viaModule}}); + reexports.push({name, declaration: {node: decl.node, known: null, viaModule}}); } else { - reexports.push({name, declaration: {node: null, expression: decl.expression, viaModule}}); + reexports.push( + {name, declaration: {node: null, known: null, expression: decl.expression, viaModule}}); } }); return reexports; @@ -213,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, viaModule: importInfo.from}; + return {node: importedFile, known: null, viaModule: importInfo.from}; } private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index dd770ae792..89d28e2398 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import {AbsoluteFsPath, FileSystem, NgtscCompilerHost, absoluteFrom} from '../../../src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system'; import {PathMappings} from '../utils'; import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPoint, EntryPointFormat} from './entry_point'; @@ -50,8 +50,7 @@ export function makeEntryPointBundle( const rootDir = entryPoint.package; const options: ts.CompilerOptions = { allowJs: true, - maxNodeModuleJsDepth: Infinity, - noLib: true, rootDir, ...pathMappings + maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings }; const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.path); const dtsHost = new NgtscCompilerHost(fs, options); 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 fa67d84d48..a17de8642b 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -1814,6 +1814,86 @@ runInEachFileSystem(() => { 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()', () => { diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 4169641809..7d2ad8eee9 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -146,6 +146,49 @@ runInEachFileSystem(() => { '{ bar: [{ type: Input }] }); })();'); }); + ['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'; + + 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: 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;`); + + 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', () => { compileIntoFlatEs5Package('test-package', { '/index.ts': ` diff --git a/packages/compiler-cli/ngcc/test/integration/util.ts b/packages/compiler-cli/ngcc/test/integration/util.ts index 0f5bd9161f..32a42a2078 100644 --- a/packages/compiler-cli/ngcc/test/integration/util.ts +++ b/packages/compiler-cli/ngcc/test/integration/util.ts @@ -109,13 +109,15 @@ function compileIntoFlatPackage( * All generated code is written into the `node_modules` in the top-level filesystem, ready for use * in testing ngcc. */ -export function compileIntoApf(pkgName: string, sources: PackageSources): void { +export function compileIntoApf( + pkgName: string, sources: PackageSources, extraCompilerOptions: ts.CompilerOptions = {}): void { const fs = getFileSystem(); const {rootNames, compileFs} = setupCompileFs(sources); const emit = (options: ts.CompilerOptions) => { const host = new MockCompilerHost(compileFs); - const program = ts.createProgram({host, rootNames, options}); + const program = + ts.createProgram({host, rootNames, options: {...extraCompilerOptions, ...options}}); program.emit(); }; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts index 6e40825840..36636f7919 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts @@ -12,25 +12,25 @@ import {DynamicValue} from './dynamic'; import {BuiltinFn, ResolvedValue, ResolvedValueArray} from './result'; export class ArraySliceBuiltinFn extends BuiltinFn { - constructor(private node: ts.Node, private lhs: ResolvedValueArray) { super(); } + constructor(private lhs: ResolvedValueArray) { super(); } - evaluate(args: ResolvedValueArray): ResolvedValue { + evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue { if (args.length === 0) { return this.lhs; } else { - return DynamicValue.fromUnknown(this.node); + return DynamicValue.fromUnknown(node); } } } export class ArrayConcatBuiltinFn extends BuiltinFn { - constructor(private node: ts.Node, private lhs: ResolvedValueArray) { super(); } + constructor(private lhs: ResolvedValueArray) { super(); } - evaluate(args: ResolvedValueArray): ResolvedValue { + evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue { const result: ResolvedValueArray = [...this.lhs]; for (const arg of args) { if (arg instanceof DynamicValue) { - result.push(DynamicValue.fromDynamicInput(this.node, arg)); + result.push(DynamicValue.fromDynamicInput(node, arg)); } else if (Array.isArray(arg)) { result.push(...arg); } else { @@ -40,3 +40,23 @@ export class ArrayConcatBuiltinFn extends BuiltinFn { return result; } } + +export class ObjectAssignBuiltinFn extends BuiltinFn { + evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue { + if (args.length === 0) { + return DynamicValue.fromUnsupportedSyntax(node); + } + for (const arg of args) { + if (arg instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, arg); + } else if (!(arg instanceof Map)) { + return DynamicValue.fromUnsupportedSyntax(node); + } + } + const [target, ...sources] = args as Map[]; + for (const source of sources) { + source.forEach((value, key) => target.set(key, value)); + } + return target; + } +} 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 f038b74e70..21e3e04985 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -17,6 +17,7 @@ import {isDeclaration} from '../../util/src/typescript'; import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; import {ForeignFunctionResolver} from './interface'; +import {resolveKnownDeclaration} from './known_declaration'; import {BuiltinFn, EnumValue, ResolvedModule, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result'; import {evaluateTsHelperInline} from './ts_helpers'; @@ -229,6 +230,9 @@ export class StaticInterpreter { return DynamicValue.fromUnknownIdentifier(node); } } + if (decl.known !== null) { + return resolveKnownDeclaration(decl.known); + } const declContext = {...context, ...joinModuleContext(context, node, decl)}; // The identifier's declaration is either concrete (a ts.Declaration exists for it) or inline // (a direct reference to a ts.Expression). @@ -357,9 +361,9 @@ export class StaticInterpreter { if (rhs === 'length') { return lhs.length; } else if (rhs === 'slice') { - return new ArraySliceBuiltinFn(node, lhs); + return new ArraySliceBuiltinFn(lhs); } else if (rhs === 'concat') { - return new ArrayConcatBuiltinFn(node, lhs); + return new ArrayConcatBuiltinFn(lhs); } if (typeof rhs !== 'number' || !Number.isInteger(rhs)) { return DynamicValue.fromInvalidExpressionType(node, rhs); @@ -401,7 +405,7 @@ export class StaticInterpreter { // If the call refers to a builtin function, attempt to evaluate the function. if (lhs instanceof BuiltinFn) { - return lhs.evaluate(this.evaluateFunctionArguments(node, context)); + return lhs.evaluate(node, this.evaluateFunctionArguments(node, context)); } if (!(lhs instanceof Reference)) { 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 new file mode 100644 index 0000000000..0ba3484afe --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/known_declaration.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {KnownDeclaration} from '../../reflection/src/host'; + +import {ObjectAssignBuiltinFn} from './builtin'; +import {ResolvedValue} from './result'; + +/** Resolved value for the JavaScript global `Object` declaration .*/ +export const jsGlobalObjectValue = new Map([['assign', new ObjectAssignBuiltinFn()]]); + +/** + * 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`. + */ +export function resolveKnownDeclaration(decl: KnownDeclaration): ResolvedValue { + switch (decl) { + case KnownDeclaration.JsGlobalObject: + return jsGlobalObjectValue; + default: + throw new Error(`Cannot resolve known declaration. Received: ${KnownDeclaration[decl]}.`); + } +} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts index 0cacefbc74..aafd8b31dd 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts @@ -78,4 +78,6 @@ export class EnumValue { /** * An implementation of a builtin function, such as `Array.prototype.slice`. */ -export abstract class BuiltinFn { abstract evaluate(args: ResolvedValueArray): ResolvedValue; } +export abstract class BuiltinFn { + abstract evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue; +} 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 055586b327..c4e894bfb1 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 @@ -10,17 +10,29 @@ import * as ts from 'typescript'; import {TsHelperFn} from '../../reflection'; +import {ObjectAssignBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; import {ResolvedValue, ResolvedValueArray} from './result'; + +/** + * Instance of the `Object.assign` builtin function. Used for evaluating + * the "__assign" TypeScript helper. + */ +const objectAssignBuiltinFn = new ObjectAssignBuiltinFn(); + export function evaluateTsHelperInline( - helper: TsHelperFn, node: ts.Node, args: ResolvedValueArray): ResolvedValue { + 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 unknown helper ${helper} inline`); + throw new Error(`Cannot evaluate TypeScript helper function: ${TsHelperFn[helper]}`); } } 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 218a371ee1..68495ddb08 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 @@ -580,6 +580,29 @@ runInEachFileSystem(() => { expect(value).toEqual([1, 2, 3]); }); + 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('(visited file tracking)', () => { it('should track each time a source file is visited', () => { const addDependency = jasmine.createSpy('DependencyTracker'); @@ -666,6 +689,8 @@ runInEachFileSystem(() => { const name = node.name !== undefined && ts.isIdentifier(node.name) && node.name.text; switch (name) { + case '__assign': + return TsHelperFn.Assign; case '__spread': return TsHelperFn.Spread; case '__spreadArrays': diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index 74c0a5e598..431930550b 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -332,6 +332,10 @@ export interface FunctionDefinition { * Possible functions from TypeScript's helper library. */ export enum TsHelperFn { + /** + * Indicates the `__assign` function. + */ + Assign, /** * Indicates the `__spread` function. */ @@ -342,6 +346,16 @@ export enum TsHelperFn { SpreadArrays, } +/** + * Possible declarations which are known. + */ +export enum KnownDeclaration { + /** + * Indicates the JavaScript global `Object` class. + */ + JsGlobalObject, +} + /** * A parameter to a function or method. */ @@ -395,6 +409,11 @@ export interface BaseDeclaration { * TypeScript reference to the declaration itself, if one exists. */ node: T|null; + + /** + * If set, describes the type of the known declaration this declaration resolves to. + */ + known: KnownDeclaration|null; } /** diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index c9c7fd779b..f8ed94d344 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -308,12 +308,12 @@ export class TypeScriptReflectionHost implements ReflectionHost { if (symbol.valueDeclaration !== undefined) { return { node: symbol.valueDeclaration, - viaModule, + known: null, viaModule, }; } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { return { node: symbol.declarations[0], - viaModule, + known: null, viaModule, }; } else { return null; diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts index 475d2451aa..9427afb376 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts @@ -362,6 +362,7 @@ runInEachFileSystem(() => { const decl = host.getDeclarationOfIdentifier(Target); expect(decl).toEqual({ node: targetDecl, + known: null, viaModule: 'absolute', }); }); @@ -391,6 +392,7 @@ runInEachFileSystem(() => { const decl = host.getDeclarationOfIdentifier(Target); expect(decl).toEqual({ node: targetDecl, + known: null, viaModule: 'absolute', }); });