Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function isDynamicValue(value: any): value is DynamicValue {
* available statically.
*/
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
ResolvedValueArray | ResolvedValueMap | DynamicValue;
ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue;

/**
* An array of `ResolvedValue`s.
Expand All @@ -81,6 +81,23 @@ export class EnumValue {
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
* body.
Expand Down Expand Up @@ -536,6 +553,8 @@ class StaticInterpreter {
} else if (Array.isArray(lhs)) {
if (rhs === 'length') {
return lhs.length;
} else if (rhs === 'slice') {
return new ArraySliceBuiltinFn(lhs);
}
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
return DYNAMIC_VALUE;
Expand Down Expand Up @@ -571,6 +590,15 @@ class StaticInterpreter {

private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue {
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)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`);
} else if (!isFunctionOrMethodReference(lhs)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ describe('ngtsc metadata', () => {
it('array `length` property access works',
() => { 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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the test use a['slice'] instead of a.slice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from above. I can change it as it isn't really relevant here.

});

it('negation works', () => {
expect(evaluate(`const x = 3;`, '!x')).toEqual(false);
expect(evaluate(`const x = 3;`, '!!x')).toEqual(true);
Expand Down