diff --git a/Userland/Libraries/LibJS/Runtime/Value.cpp b/Userland/Libraries/LibJS/Runtime/Value.cpp index ba18cc10e3..61bccf04cc 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.cpp +++ b/Userland/Libraries/LibJS/Runtime/Value.cpp @@ -978,6 +978,51 @@ ThrowCompletionOr unsigned_right_shift(GlobalObject& global_object, Value return vm.throw_completion(global_object, ErrorType::BigIntBadOperator, "unsigned right-shift"); } +// https://tc39.es/ecma262/#string-concatenation +static PrimitiveString* concatenate_strings(GlobalObject& global_object, PrimitiveString const& lhs, PrimitiveString const& rhs) +{ + auto& vm = global_object.vm(); + + if (lhs.has_utf16_string() && rhs.has_utf16_string()) { + auto const& lhs_string = lhs.utf16_string(); + auto const& rhs_string = rhs.utf16_string(); + + Vector combined; + combined.ensure_capacity(lhs_string.length_in_code_units() + rhs_string.length_in_code_units()); + combined.extend(lhs_string.string()); + combined.extend(rhs_string.string()); + + return js_string(vm, Utf16String(move(combined))); + } + + Utf8View lhs_string { lhs.string() }; + Utf8View rhs_string { rhs.string() }; + + StringBuilder builder(lhs_string.length() + rhs_string.length()); + Optional high_surrogate; + + for (auto it = lhs_string.begin(); it != lhs_string.end(); ++it) { + if (!it.peek(1).has_value() && Utf16View::is_high_surrogate(*it) && !rhs_string.is_empty()) + high_surrogate = *it; + else + builder.append_code_point(*it); + } + + if (high_surrogate.has_value()) { + auto low_surrogate = *rhs_string.begin(); + + if (Utf16View::is_low_surrogate(low_surrogate)) { + builder.append_code_point(Utf16View::decode_surrogate_pair(*high_surrogate, low_surrogate)); + rhs_string = rhs_string.substring_view(3); // A low surrogate encoded as UTF-8 is 3 bytes. + } else { + builder.append_code_point(*high_surrogate); + } + } + + builder.append(rhs_string.as_string()); + return js_string(vm, builder.to_string()); +} + // 13.8.1 The Addition Operator ( + ), https://tc39.es/ecma262/#sec-addition-operator-plus ThrowCompletionOr add(GlobalObject& global_object, Value lhs, Value rhs) { @@ -995,28 +1040,10 @@ ThrowCompletionOr add(GlobalObject& global_object, Value lhs, Value rhs) auto lhs_primitive = TRY(lhs.to_primitive(global_object)); auto rhs_primitive = TRY(rhs.to_primitive(global_object)); - if (lhs_primitive.is_string() && rhs_primitive.is_string()) { - auto const& lhs_string = lhs_primitive.as_string(); - auto const& rhs_string = rhs_primitive.as_string(); - - if (lhs_string.has_utf16_string() && rhs_string.has_utf16_string()) { - auto const& lhs_utf16_string = lhs_string.utf16_string(); - auto const& rhs_utf16_string = rhs_string.utf16_string(); - - Vector combined; - combined.ensure_capacity(lhs_utf16_string.length_in_code_units() + rhs_utf16_string.length_in_code_units()); - combined.extend(lhs_utf16_string.string()); - combined.extend(rhs_utf16_string.string()); - return Value(js_string(vm.heap(), Utf16String(move(combined)))); - } - } if (lhs_primitive.is_string() || rhs_primitive.is_string()) { - auto lhs_string = TRY(lhs_primitive.to_string(global_object)); - auto rhs_string = TRY(rhs_primitive.to_string(global_object)); - StringBuilder builder(lhs_string.length() + rhs_string.length()); - builder.append(lhs_string); - builder.append(rhs_string); - return Value(js_string(vm, builder.to_string())); + auto lhs_string = TRY(lhs_primitive.to_primitive_string(global_object)); + auto rhs_string = TRY(rhs_primitive.to_primitive_string(global_object)); + return concatenate_strings(global_object, *lhs_string, *rhs_string); } auto lhs_numeric = TRY(lhs_primitive.to_numeric(global_object)); diff --git a/Userland/Libraries/LibJS/Tests/string-concatenation.js b/Userland/Libraries/LibJS/Tests/string-concatenation.js new file mode 100644 index 0000000000..3f93483077 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/string-concatenation.js @@ -0,0 +1,30 @@ +test("adding strings", () => { + expect("" + "").toBe(""); + expect("ab" + "").toBe("ab"); + expect("" + "cd").toBe("cd"); + expect("ab" + "cd").toBe("abcd"); +}); + +test("adding strings with non-strings", () => { + expect("a" + 1).toBe("a1"); + expect(1 + "a").toBe("1a"); + expect("a" + {}).toBe("a[object Object]"); + expect({} + "a").toBeNaN(); + expect("a" + []).toBe("a"); + expect([] + "a").toBe("a"); + expect("a" + NaN).toBe("aNaN"); + expect(NaN + "a").toBe("NaNa"); + expect(Array(16).join([[][[]] + []][+[]][++[+[]][+[]]] - 1) + " Batman!").toBe( + "NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!" + ); +}); + +test("adding strings with dangling surrogates", () => { + expect("\ud834" + "").toBe("\ud834"); + expect("" + "\udf06").toBe("\udf06"); + expect("\ud834" + "\udf06").toBe("𝌆"); + expect("\ud834" + "\ud834").toBe("\ud834\ud834"); + expect("\udf06" + "\udf06").toBe("\udf06\udf06"); + expect("\ud834a" + "\udf06").toBe("\ud834a\udf06"); + expect("\ud834" + "a\udf06").toBe("\ud834a\udf06"); +});