Skip to content

Commit

Permalink
Add support for string concatenation with +.
Browse files Browse the repository at this point in the history
This isn't strictly necessary, since we already support string interpolation. But Velocity supports `+` so this will help porting to EscapeVelocity when templates use `+`.

RELNOTES=String concatenation with `+` is now supported. (String interpolation was already supported.)
PiperOrigin-RevId: 498000362
  • Loading branch information
eamonnmcmanus authored and EscapeVelocity Team committed Dec 27, 2022
1 parent d2c4514 commit c6e256a
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 2 deletions.
33 changes: 31 additions & 2 deletions src/main/java/com/google/escapevelocity/ExpressionNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ private String operandString(ExpressionNode operand) {
return equal(context);
case NOT_EQUAL:
return !equal(context);
case PLUS:
return plus(context);
default: // fall out
}
Integer lhsInt = lhs.intValue(context);
Expand All @@ -205,8 +207,6 @@ private String operandString(ExpressionNode operand) {
return lhsInt > rhsInt;
case GREATER_OR_EQUAL:
return lhsInt >= rhsInt;
case PLUS:
return lhsInt + rhsInt;
case MINUS:
return lhsInt - rhsInt;
case TIMES:
Expand Down Expand Up @@ -258,6 +258,35 @@ private boolean equal(EvaluationContext context) {
// Funky equals behaviour specified by Velocity.
return lhsValue.toString().equals(rhsValue.toString());
}

private Object plus(EvaluationContext context) {
Object lhsValue = lhs.evaluate(context);
Object rhsValue = rhs.evaluate(context);
if (lhsValue instanceof String || rhsValue instanceof String) {
// Velocity's treatment of null is all over the map. In a string concatenation, a null
// reference is replaced by the the source text of the reference, for example "$foo". The
// toString() that we have for the various ExpressionNode subtypes reproduces this at least
// in our test cases.
if (lhsValue == null) {
lhsValue = lhs.toString();
}
if (rhsValue == null) {
rhsValue = rhs.toString();
}
return new StringBuilder().append(lhsValue).append(rhsValue).toString();
}
if (lhsValue == null || rhsValue == null) {
return null;
}
if (!(lhsValue instanceof Integer) || !(rhsValue instanceof Integer)) {
throw evaluationException(
"Operands of + must both be integers, or at least one must be a string: "
+ show(lhsValue)
+ " + "
+ show(rhsValue));
}
return (Integer) lhsValue + (Integer) rhsValue;
}
}

/**
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/com/google/escapevelocity/TemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,20 @@ public void expressions() {
compare("#set ($x = 22 - 7) $x");
compare("#set ($x = 22 / 7) $x");
compare("#set ($x = 22 % 7) $x");

compare("#set ($x = 'foo' + 'bar') $x");
compare("#set ($x = 23 + ' skidoo') $x");
compare("#set ($x = 'heaven ' + 17) $x");

// Check that we copy Velocity here: these null references will be replaced by their source
// text, for example "$bar" for the null $bar reference in the first two.
compare("#set ($x = 'foo' + $bar) $x", Collections.singletonMap("bar", null));
compare("#set ($x = $bar + 'foo') $x", Collections.singletonMap("bar", null));

// This one results in "foo$bar + $bar" in both Velocity and EscapeVelocity. $bar + $bar is null
// and then 'foo' + null gets replaced by a representation of the source expression that
// produced the null.
compare("#set ($x = 'foo' + ($bar + $bar)) $x", Collections.singletonMap("bar", null));
}

@Test
Expand All @@ -600,6 +614,7 @@ public void arithmeticOperationsOnNullAreNull() {
"#nulltest($null)",
"#nulltest('not null')",
"#set ($x = 1 + $null) #nulltest($x)",
"#set ($x = $null + $null) #nulltest($x)",
"#set ($x = $null - 1) #nulltest($x)",
"#set ($x = $null * $null) #nulltest($x)",
"#set ($x = $null / $null) #nulltest($x)",
Expand Down

0 comments on commit c6e256a

Please sign in to comment.