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', }); });