diff --git a/Jint.Tests/Runtime/StringTests.cs b/Jint.Tests/Runtime/StringTests.cs index a4ef7d4bbe..886cdeac95 100644 --- a/Jint.Tests/Runtime/StringTests.cs +++ b/Jint.Tests/Runtime/StringTests.cs @@ -12,6 +12,22 @@ public StringTests() private readonly Engine _engine; + [Fact] + public void MixedTypeAdditionShouldEvaluateLeftToRight() + { + var engine = new Engine(); + // Numbers before a string literal must be added numerically first + Assert.Equal("5m", engine.Evaluate("2.0 + 3.0 + 'm'").AsString()); + Assert.Equal("5m", engine.Evaluate("2 + 3 + 'm'").AsString()); + Assert.Equal("5mx", engine.Evaluate("2.0 + 3.0 + 'm' + 'x'").AsString()); + Assert.Equal("64", engine.Evaluate("1 + 2 + 3 + '4'").AsString()); + + // String literal first: all ops are string concatenation + Assert.Equal("m23", engine.Evaluate("'m' + 2 + 3").AsString()); + // String literal at index 1: correct too + Assert.Equal("2m3", engine.Evaluate("2 + 'm' + 3").AsString()); + } + [Fact] public void StringConcatenationAndReferences() { diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs index 24020634ac..64b44f83e3 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs @@ -225,18 +225,11 @@ private static bool TryBuildStringConcatenation( var operands = new List(); CollectAdditionOperands(expression, operands); - // Must have at least one string literal to guarantee string concatenation semantics - var hasStringLiteral = false; - foreach (var operand in operands) - { - if (operand is Literal { Value: string }) - { - hasStringLiteral = true; - break; - } - } - - if (!hasStringLiteral) + // Must have a string literal in the first two operands to guarantee string concatenation semantics + // from the very first operation. A string literal at index 2+ is not enough because earlier operands + // could be numerically added (e.g., 2.0 + 3.0 + 'm' should yield '5m', not '23m'). + // The early return above guarantees at least 3 operands, so operands[0] and operands[1] are always valid. + if (operands.Count < 2 || (operands[0] is not Literal { Value: string } && operands[1] is not Literal { Value: string })) { return false; }