Skip to content

Commit

Permalink
LibJS: Make (most) String.prototype functions generic
Browse files Browse the repository at this point in the history
I.e. they don't require the |this| value to be a string object and
"can be transferred to other kinds of objects for use as a method" as
the spec describes it.
  • Loading branch information
linusg authored and awesomekling committed Apr 29, 2020
1 parent 4bdb6da commit cfdb7b8
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 89 deletions.
164 changes: 75 additions & 89 deletions Libraries/LibJS/Runtime/StringPrototype.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Andreas Kling <[email protected]>
* Copyright (c) 2020, Linus Groh <[email protected]>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -38,6 +39,26 @@

namespace JS {

static StringObject* string_object_from(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
return nullptr;
if (!this_object->is_string_object()) {
interpreter.throw_exception<TypeError>("Not a String object");
return nullptr;
}
return static_cast<StringObject*>(this_object);
}

static String string_from(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
return {};
return Value(this_object).to_string();
}

StringPrototype::StringPrototype()
: StringObject(*js_string(interpreter(), String::empty()), *interpreter().global_object().object_prototype())
{
Expand All @@ -53,7 +74,6 @@ StringPrototype::StringPrototype()
put_native_function("toString", to_string, 0, attr);
put_native_function("padStart", pad_start, 1, attr);
put_native_function("padEnd", pad_end, 1, attr);

put_native_function("trim", trim, 0, attr);
put_native_function("trimStart", trim_start, 0, attr);
put_native_function("trimEnd", trim_end, 0, attr);
Expand All @@ -69,43 +89,40 @@ StringPrototype::~StringPrototype()

Value StringPrototype::char_at(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
i32 index = 0;
if (interpreter.argument_count())
index = interpreter.argument(0).to_i32();
ASSERT(this_object->is_string_object());
auto underlying_string = static_cast<const StringObject*>(this_object)->primitive_string().string();
if (index < 0 || index >= static_cast<i32>(underlying_string.length()))
if (index < 0 || index >= static_cast<i32>(string.length()))
return js_string(interpreter, String::empty());
return js_string(interpreter, underlying_string.substring(index, 1));
return js_string(interpreter, string.substring(index, 1));
}

Value StringPrototype::repeat(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
ASSERT(this_object->is_string_object());
if (!interpreter.argument_count())
return js_string(interpreter, String::empty());
if (interpreter.argument(0).to_double() < 0)
auto count_value = interpreter.argument(0).to_number();
if (count_value.as_double() < 0)
return interpreter.throw_exception<RangeError>("repeat count must be a positive number");
if (interpreter.argument(0).to_number().is_infinity())
if (count_value.is_infinity())
return interpreter.throw_exception<RangeError>("repeat count must be a finite number");
auto count = interpreter.argument(0).to_i32();
auto& string_object = static_cast<const StringObject&>(*this_object);
auto count = count_value.to_i32();
StringBuilder builder;
for (i32 i = 0; i < count; ++i)
builder.append(string_object.primitive_string().string());
builder.append(string);
return js_string(interpreter, builder.to_string());
}

Value StringPrototype::starts_with(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
if (!interpreter.argument_count())
return Value(false);
Expand All @@ -117,59 +134,41 @@ Value StringPrototype::starts_with(Interpreter& interpreter)
if (!number.is_nan())
position = number.to_i32();
}
ASSERT(this_object->is_string_object());
auto underlying_string = static_cast<const StringObject*>(this_object)->primitive_string().string();
auto underlying_string_length = static_cast<i32>(underlying_string.length());
auto start = min(max(position, 0), underlying_string_length);
if (start + search_string_length > underlying_string_length)
auto string_length = static_cast<i32>(string.length());
auto start = min(max(position, 0), string_length);
if (start + search_string_length > string_length)
return Value(false);
if (search_string_length == 0)
return Value(true);
return Value(underlying_string.substring(start, search_string_length) == search_string);
return Value(string.substring(start, search_string_length) == search_string);
}

Value StringPrototype::index_of(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
if (!this_object->is_string_object())
return interpreter.throw_exception<TypeError>("Not a String object");

Value needle_value = js_undefined();
if (interpreter.argument_count() >= 1)
needle_value = interpreter.argument(0);
auto needle = needle_value.to_string();
auto haystack = static_cast<const StringObject*>(this_object)->primitive_string().string();
return Value((i32)haystack.index_of(needle).value_or(-1));
}

static StringObject* string_object_from(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
return nullptr;
if (!this_object->is_string_object()) {
interpreter.throw_exception<TypeError>("Not a String object");
return nullptr;
}
return static_cast<StringObject*>(this_object);
return Value((i32)string.index_of(needle).value_or(-1));
}

Value StringPrototype::to_lowercase(Interpreter& interpreter)
{
auto* string_object = string_object_from(interpreter);
if (!string_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return js_string(interpreter, string_object->primitive_string().string().to_lowercase());
return js_string(interpreter, string.to_lowercase());
}

Value StringPrototype::to_uppercase(Interpreter& interpreter)
{
auto* string_object = string_object_from(interpreter);
if (!string_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return js_string(interpreter, string_object->primitive_string().string().to_uppercase());
return js_string(interpreter, string.to_uppercase());
}

Value StringPrototype::length_getter(Interpreter& interpreter)
Expand All @@ -193,15 +192,13 @@ enum class PadPlacement {
End,
};

static Value pad_string(Interpreter& interpreter, Object* object, PadPlacement placement)
static Value pad_string(Interpreter& interpreter, const String& string, PadPlacement placement)
{
auto string = object->to_string().as_string().string();
if (interpreter.argument(0).to_number().is_nan()
|| interpreter.argument(0).to_number().is_undefined()
|| interpreter.argument(0).to_number().to_i32() < 0) {
auto max_length_value = interpreter.argument(0).to_number();
if (max_length_value.is_nan() || max_length_value.is_undefined() || max_length_value.as_double() < 0)
return js_string(interpreter, string);
}
auto max_length = static_cast<size_t>(interpreter.argument(0).to_i32());

auto max_length = static_cast<size_t>(max_length_value.to_i32());
if (max_length <= string.length())
return js_string(interpreter, string);

Expand All @@ -225,18 +222,18 @@ static Value pad_string(Interpreter& interpreter, Object* object, PadPlacement p

Value StringPrototype::pad_start(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return pad_string(interpreter, this_object, PadPlacement::Start);
return pad_string(interpreter, string, PadPlacement::Start);
}

Value StringPrototype::pad_end(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return pad_string(interpreter, this_object, PadPlacement::End);
return pad_string(interpreter, string, PadPlacement::End);
}

enum class TrimMode {
Expand All @@ -245,10 +242,8 @@ enum class TrimMode {
Both
};

static Value trim_string(Interpreter& interpreter, const Object& object, TrimMode mode)
static Value trim_string(Interpreter& interpreter, const String& string, TrimMode mode)
{
auto& string = object.to_string().as_string().string();

size_t substring_start = 0;
size_t substring_length = string.length();

Expand All @@ -267,7 +262,7 @@ static Value trim_string(Interpreter& interpreter, const Object& object, TrimMod
}

if (substring_length == 0)
return js_string(interpreter, String(""));
return js_string(interpreter, "");

if (mode == TrimMode::Right || mode == TrimMode::Both) {
size_t count = 0;
Expand All @@ -285,54 +280,47 @@ static Value trim_string(Interpreter& interpreter, const Object& object, TrimMod

Value StringPrototype::trim(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return trim_string(interpreter, *this_object, TrimMode::Both);
return trim_string(interpreter, string, TrimMode::Both);
}

Value StringPrototype::trim_start(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return trim_string(interpreter, *this_object, TrimMode::Left);
return trim_string(interpreter, string, TrimMode::Left);
}

Value StringPrototype::trim_end(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
return trim_string(interpreter, *this_object, TrimMode::Right);
return trim_string(interpreter, string, TrimMode::Right);
}

Value StringPrototype::concat(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};
auto& string = this_object->to_string().as_string().string();

StringBuilder builder;
builder.append(string);

for (size_t i = 0; i < interpreter.argument_count(); ++i) {
auto string_argument = interpreter.argument(i).to_string();
builder.append(string_argument);
}

return js_string(interpreter, builder.to_string());
}

Value StringPrototype::substring(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};

auto& string = this_object->to_string().as_string().string();

if (interpreter.argument_count() == 0)
return js_string(interpreter, string);

Expand Down Expand Up @@ -374,11 +362,9 @@ Value StringPrototype::substring(Interpreter& interpreter)

Value StringPrototype::includes(Interpreter& interpreter)
{
auto* this_object = interpreter.this_value().to_object(interpreter.heap());
if (!this_object)
auto string = string_from(interpreter);
if (string.is_null())
return {};

auto& string = this_object->to_string().as_string().string();
auto search_string = interpreter.argument(0).to_string();
i32 position = 0;

Expand Down
51 changes: 51 additions & 0 deletions Libraries/LibJS/Tests/String.prototype-generic-functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
load("test-common.js");

try {
const genericStringPrototypeFunctions = [
"charAt",
"repeat",
"startsWith",
"indexOf",
"toLowerCase",
"toUpperCase",
"padStart",
"padEnd",
"trim",
"trimStart",
"trimEnd",
"concat",
"substring",
"includes",
];

genericStringPrototypeFunctions.forEach(name => {
String.prototype[name].call({ toString: () => "hello friends" });
String.prototype[name].call({ toString: () => 123 });
String.prototype[name].call({ toString: () => undefined });

assertThrowsError(() => {
String.prototype[name].call({ toString: () => new String() });
}, {
error: TypeError,
message: "Cannot convert object to string"
});

assertThrowsError(() => {
String.prototype[name].call({ toString: () => [] });
}, {
error: TypeError,
message: "Cannot convert object to string"
});

assertThrowsError(() => {
String.prototype[name].call({ toString: () => ({}) });
}, {
error: TypeError,
message: "Cannot convert object to string"
});
});

console.log("PASS");
} catch (err) {
console.log("FAIL: " + err);
}

0 comments on commit cfdb7b8

Please sign in to comment.