feat(ivy): ngtsc support for static resolution of array.slice() (#27158)

For ngcc's processing of ES5 bundles, the spread syntax has been
downleveled from `[...ARRAY]` to become `ARRAY.slice()`. This commit
adds basic support for static resolution of such call.

PR Close #27158
This commit is contained in:
JoostK 2018-11-18 21:14:48 +01:00 committed by Alex Rickabaugh
parent 06d4a0c46e
commit 75723d5c89
2 changed files with 33 additions and 1 deletions

View File

@ -54,7 +54,7 @@ export function isDynamicValue(value: any): value is DynamicValue {
* available statically. * available statically.
*/ */
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue | export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
ResolvedValueArray | ResolvedValueMap | DynamicValue; ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue;
/** /**
* An array of `ResolvedValue`s. * An array of `ResolvedValue`s.
@ -81,6 +81,23 @@ export class EnumValue {
constructor(readonly enumRef: Reference<ts.EnumDeclaration>, readonly name: string) {} constructor(readonly enumRef: Reference<ts.EnumDeclaration>, readonly name: string) {}
} }
/**
* An implementation of a builtin function, such as `Array.prototype.slice`.
*/
export abstract class BuiltinFn { abstract evaluate(args: ResolvedValueArray): ResolvedValue; }
class ArraySliceBuiltinFn extends BuiltinFn {
constructor(private lhs: ResolvedValueArray) { super(); }
evaluate(args: ResolvedValueArray): ResolvedValue {
if (args.length === 0) {
return this.lhs;
} else {
return DYNAMIC_VALUE;
}
}
}
/** /**
* Tracks the scope of a function body, which includes `ResolvedValue`s for the parameters of that * Tracks the scope of a function body, which includes `ResolvedValue`s for the parameters of that
* body. * body.
@ -536,6 +553,8 @@ class StaticInterpreter {
} else if (Array.isArray(lhs)) { } else if (Array.isArray(lhs)) {
if (rhs === 'length') { if (rhs === 'length') {
return lhs.length; return lhs.length;
} else if (rhs === 'slice') {
return new ArraySliceBuiltinFn(lhs);
} }
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) { if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
return DYNAMIC_VALUE; return DYNAMIC_VALUE;
@ -571,6 +590,15 @@ class StaticInterpreter {
private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue { private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue {
const lhs = this.visitExpression(node.expression, context); const lhs = this.visitExpression(node.expression, context);
if (isDynamicValue(lhs)) {
return DYNAMIC_VALUE;
}
// If the call refers to a builtin function, attempt to evaluate the function.
if (lhs instanceof BuiltinFn) {
return lhs.evaluate(node.arguments.map(arg => this.visitExpression(arg, context)));
}
if (!(lhs instanceof Reference)) { if (!(lhs instanceof Reference)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`); throw new Error(`attempting to call something that is not a function: ${lhs}`);
} else if (!isFunctionOrMethodReference(lhs)) { } else if (!isFunctionOrMethodReference(lhs)) {

View File

@ -118,6 +118,10 @@ describe('ngtsc metadata', () => {
it('array `length` property access works', it('array `length` property access works',
() => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); }); () => { expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); });
it('array `slice` function works', () => {
expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]);
});
it('negation works', () => { it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false); expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true); expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);