fix(compiler): resolve enum values in binary operations (#36461)

During static evaluation of expressions, the partial evaluator
may come across a binary + operator for which it needs to
evaluate its operands. Any of these operands may be a reference
to an enum member, in which case the enum member's value needs
to be used as literal value, not the enum member reference
itself. This commit fixes the behavior by resolving an
`EnumValue` when used as a literal value.

Fixes #35584
Resolves FW-1951

PR Close #36461
This commit is contained in:
JoostK 2020-04-06 22:33:38 +02:00 committed by Kara Erickson
parent f9f6e2e1b3
commit 64022f51d4
2 changed files with 42 additions and 21 deletions

View File

@ -203,19 +203,13 @@ export class StaticInterpreter {
const pieces: string[] = [node.head.text]; const pieces: string[] = [node.head.text];
for (let i = 0; i < node.templateSpans.length; i++) { for (let i = 0; i < node.templateSpans.length; i++) {
const span = node.templateSpans[i]; const span = node.templateSpans[i];
let value = this.visit(span.expression, context); const value = literal(
if (value instanceof EnumValue) { this.visit(span.expression, context),
value = value.resolved; () => DynamicValue.fromDynamicString(span.expression));
} if (value instanceof DynamicValue) {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||
value == null) {
pieces.push(`${value}`);
} else if (value instanceof DynamicValue) {
return DynamicValue.fromDynamicInput(node, value); return DynamicValue.fromDynamicInput(node, value);
} else {
return DynamicValue.fromDynamicInput(node, DynamicValue.fromDynamicString(span.expression));
} }
pieces.push(span.literal.text); pieces.push(`${value}`, span.literal.text);
} }
return pieces.join(''); return pieces.join('');
} }
@ -520,8 +514,12 @@ export class StaticInterpreter {
const opRecord = BINARY_OPERATORS.get(tokenKind)!; const opRecord = BINARY_OPERATORS.get(tokenKind)!;
let lhs: ResolvedValue, rhs: ResolvedValue; let lhs: ResolvedValue, rhs: ResolvedValue;
if (opRecord.literal) { if (opRecord.literal) {
lhs = literal(this.visitExpression(node.left, context), node.left); lhs = literal(
rhs = literal(this.visitExpression(node.right, context), node.right); this.visitExpression(node.left, context),
value => DynamicValue.fromInvalidExpressionType(node.left, value));
rhs = literal(
this.visitExpression(node.right, context),
value => DynamicValue.fromInvalidExpressionType(node.right, value));
} else { } else {
lhs = this.visitExpression(node.left, context); lhs = this.visitExpression(node.left, context);
rhs = this.visitExpression(node.right, context); rhs = this.visitExpression(node.right, context);
@ -585,12 +583,16 @@ function isFunctionOrMethodReference(ref: Reference<ts.Node>):
ts.isFunctionExpression(ref.node); ts.isFunctionExpression(ref.node);
} }
function literal(value: ResolvedValue, node: ts.Node): any { function literal(
value: ResolvedValue, reject: (value: ResolvedValue) => ResolvedValue): ResolvedValue {
if (value instanceof EnumValue) {
value = value.resolved;
}
if (value instanceof DynamicValue || value === null || value === undefined || if (value instanceof DynamicValue || value === null || value === undefined ||
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value; return value;
} }
return DynamicValue.fromInvalidExpressionType(node, value); return reject(value);
} }
function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean { function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {

View File

@ -448,6 +448,31 @@ runInEachFileSystem(() => {
expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345'); expect(evaluate('const a = 2, b = 4;', '`1${a}3${b}5`')).toEqual('12345');
}); });
it('template expressions should resolve enums', () => {
expect(evaluate('enum Test { VALUE = "test" };', '`a.${Test.VALUE}.b`')).toBe('a.test.b');
});
it('string concatenation should resolve enums', () => {
expect(evaluate('enum Test { VALUE = "test" };', '"a." + Test.VALUE + ".b"'))
.toBe('a.test.b');
});
it('should resolve non-literals as dynamic string', () => {
const value = evaluate(`const a: any = [];`, '`a.${a}.b`');
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
expect(value.node.getText()).toEqual('`a.${a}.b`');
if (!value.isFromDynamicInput()) {
return fail('Should originate from dynamic input');
} else if (!value.reason.isFromDynamicString()) {
return fail('Should refer to a dynamic string part');
}
expect(value.reason.node.getText()).toEqual('a');
});
it('enum resolution works', () => { it('enum resolution works', () => {
const result = evaluate( const result = evaluate(
` `
@ -512,12 +537,6 @@ runInEachFileSystem(() => {
expect(value.node).toBe(prop.initializer); expect(value.node).toBe(prop.initializer);
}); });
it('should resolve enums in template expressions', () => {
const value =
evaluate(`enum Test { VALUE = 'test', } const value = \`a.\${Test.VALUE}.b\`;`, 'value');
expect(value).toBe('a.test.b');
});
it('should not attach identifiers to FFR-resolved values', () => { it('should not attach identifiers to FFR-resolved values', () => {
const value = evaluate( const value = evaluate(
` `