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 db65b4dfd3..76b27e7a87 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -266,6 +266,8 @@ export class StaticInterpreter { return this.visitEnumDeclaration(node, context); } else if (ts.isSourceFile(node)) { return this.visitSourceFile(node, context); + } else if (ts.isBindingElement(node)) { + return this.visitBindingElement(node, context); } else { return this.getReference(node, context); } @@ -347,9 +349,8 @@ export class StaticInterpreter { }); } - private accessHelper( - node: ts.Expression, lhs: ResolvedValue, rhs: string|number, - context: Context): ResolvedValue { + private accessHelper(node: ts.Node, lhs: ResolvedValue, rhs: string|number, context: Context): + ResolvedValue { const strIndex = `${rhs}`; if (lhs instanceof Map) { if (lhs.has(strIndex)) { @@ -598,6 +599,47 @@ export class StaticInterpreter { } } + private visitBindingElement(node: ts.BindingElement, context: Context): ResolvedValue { + const path: ts.BindingElement[] = []; + let closestDeclaration: ts.Node = node; + + while (ts.isBindingElement(closestDeclaration) || + ts.isArrayBindingPattern(closestDeclaration) || + ts.isObjectBindingPattern(closestDeclaration)) { + if (ts.isBindingElement(closestDeclaration)) { + path.unshift(closestDeclaration); + } + + closestDeclaration = closestDeclaration.parent; + } + + if (!ts.isVariableDeclaration(closestDeclaration) || + closestDeclaration.initializer === undefined) { + return DynamicValue.fromUnknown(node); + } + + let value = this.visit(closestDeclaration.initializer, context); + for (const element of path) { + let key: number|string; + if (ts.isArrayBindingPattern(element.parent)) { + key = element.parent.elements.indexOf(element); + } else { + const name = element.propertyName || element.name; + if (ts.isIdentifier(name)) { + key = name.text; + } else { + return DynamicValue.fromUnknown(element); + } + } + value = this.accessHelper(element, value, key, context); + if (value instanceof DynamicValue) { + return value; + } + } + + return value; + } + private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string|undefined { if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { return node.text; 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 f64f1ea204..72695d9ad9 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 @@ -200,6 +200,58 @@ runInEachFileSystem(() => { expect(evaluate('const a = null;', 'a')).toEqual(null); }); + it('supports destructuring array variable declarations', () => { + const code = ` + const [a, b, c, d] = [0, 1, 2, 3]; + const e = c; + `; + + expect(evaluate(code, 'a')).toBe(0); + expect(evaluate(code, 'b')).toBe(1); + expect(evaluate(code, 'c')).toBe(2); + expect(evaluate(code, 'd')).toBe(3); + expect(evaluate(code, 'e')).toBe(2); + }); + + it('supports destructuring object variable declaration', () => { + const code = ` + const {a, b, c, d} = {a: 0, b: 1, c: 2, d: 3}; + const e = c; + `; + + expect(evaluate(code, 'a')).toBe(0); + expect(evaluate(code, 'b')).toBe(1); + expect(evaluate(code, 'c')).toBe(2); + expect(evaluate(code, 'd')).toBe(3); + expect(evaluate(code, 'e')).toBe(2); + }); + + it('supports destructuring object variable declaration with an alias', () => { + expect(evaluate(`const {a: value} = {a: 5}; const e = value;`, 'e')).toBe(5); + }); + + it('supports nested destructuring object variable declarations', () => { + expect(evaluate(`const {a: {b: {c}}} = {a: {b: {c: 0}}};`, 'c')).toBe(0); + }); + + it('supports nested destructuring array variable declarations', () => { + expect(evaluate(`const [[[a]]] = [[[1]]];`, 'a')).toBe(1); + }); + + it('supports nested destructuring variable declarations mixing arrays and objects', () => { + expect(evaluate(`const {a: {b: [[c]]}} = {a: {b: [[1337]]}};`, 'c')).toBe(1337); + }); + + it('resolves unknown values in a destructured variable declaration as dynamic values', () => { + const value = evaluate( + `const {a: {body}} = {a: window};`, 'body', + [{name: _('/window.ts'), contents: `declare const window: any;`}]); + if (!(value instanceof DynamicValue)) { + return fail(`Should have resolved to a DynamicValue`); + } + expect(value.node.getText()).toBe('body'); + }); + it('resolves unknown binary operators as dynamic value', () => { const value = evaluate('declare const window: any;', '"location" in window'); if (!(value instanceof DynamicValue)) {