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
This commit is contained in:

committed by
Andrew Kushnir

parent
a10d2a8dc4
commit
4eeb6cf24d
@ -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<string, ResolvedValue>[];
|
||||
for (const source of sources) {
|
||||
source.forEach((value, key) => target.set(key, value));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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]}.`);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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]}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<string, boolean>;
|
||||
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':
|
||||
|
@ -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<T extends ts.Declaration = ts.Declaration> {
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user