Skip to content

List + / - operators mutate the left operand in place (OperatorUtils.addToList / subtractFromList) #802

@loicmathieu

Description

@loicmathieu

Describe the bug

The + and - operators on lists mutate their left operand in place instead of returning a new list. OperatorUtils.addToList calls op1.addAll(op2) and returns the same op1 instance; OperatorUtils.subtractFromList calls op1.removeAll(op2) / op1.remove(op2) similarly. Any later use of the left-hand variable then sees the mutated contents.

private static Object addToList(List<?> op1, Object op2) {
  if (op2 instanceof Collection) {
    op1.addAll((Collection) op2);   // mutates the caller's list
  } else {
    ((List<Object>) op1).add(op2);
  }
  return op1;
}

To Reproduce

PebbleEngine engine = new PebbleEngine.Builder().build();
PebbleTemplate template = engine.getLiteralTemplate("{{ a + b }} | {{ a }}");
Map<String, Object> ctx = new HashMap<>();
ctx.put("a", new ArrayList<>(List.of("x")));
ctx.put("b", List.of("y", "z"));
StringWriter writer = new StringWriter();
template.evaluate(writer, ctx);
System.out.println(writer);

Expected behavior

[x, y, z] | [x] — evaluating a + b should not change a. Template expression evaluation is expected to be side-effect free.

Actual behavior

[x, y, z] | [x, y, z]a is mutated in place by the + operator. The same happens with - via subtractFromList.

Suggested fix

Make addToList / subtractFromList non-mutating by copying the left operand before modifying:

private static Object addToList(List<?> op1, Object op2) {
  List<Object> result = new ArrayList<>(op1);
  if (op2 instanceof Collection) {
    result.addAll((Collection) op2);
  } else {
    result.add(op2);
  }
  return result;
}

Both methods are already @Deprecated; if the feature is to be kept, it should at least be side-effect free.

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions