Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/twocore_rt_js_ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6180,7 +6180,7 @@ str_slice(Str, Start, End) ->

%% str.substring(start?, end?) — clamps negatives to 0 and swaps if start > end.
str_substring(Str, Start, End) ->
Cps = cps(Str),
Cps = cps(str_this(Str)),
Len = length(Cps),
S0 = sub_index(Start, Len, 0),
E0 = sub_index(End, Len, Len),
Expand Down Expand Up @@ -6227,9 +6227,13 @@ str_split(Str0, Sep, Limit) ->
%% Object]"). This is what makes `String.prototype.trim.call(0)` yield "0",
%% `.call(false)` yield "false", and `.call([1])` yield "1" once a generic `.call`
%% forwards the receiver here.
%% A Symbol receiver is a TypeError: ToString of a Symbol is abrupt (§7.1.17,
%% ToString step 2), so `String.prototype.substring.call(Symbol())` throws.
str_this(undefined) -> type_error(undefined);
str_this(null) -> type_error(null);
str_this(V) when is_binary(V) -> V;
str_this({js_symbol, _, _}) ->
type_error(<<"Cannot convert a Symbol value to a string">>);
str_this(V) -> to_string(V).

%% ToUint32(limit) with `undefined` mapped to the spec's 2^32-1 default. NaN and
Expand Down Expand Up @@ -6323,8 +6327,9 @@ str_repeat(Str, N) ->
%% ToIntegerOrInfinity clamped to [0, len] (undefined/NaN → 0). Returns false
%% when `position` + prefix length would run past the end of the string.
str_starts_with(Str, Prefix, Pos) ->
S = str_this(Str),
P = to_string(Prefix),
Cps = cps(Str),
Cps = cps(S),
Len = length(Cps),
Start = str_pos_clamp(Pos, Len),
PLen = length(cps(P)),
Expand All @@ -6338,8 +6343,9 @@ str_starts_with(Str, Prefix, Pos) ->
%% is undefined it defaults to len; otherwise it is ToIntegerOrInfinity clamped
%% to [0, len]. Returns false when the suffix would start before index 0.
str_ends_with(Str, Suffix, EndPos) ->
Recv = str_this(Str),
S = to_string(Suffix),
Cps = cps(Str),
Cps = cps(Recv),
Len = length(Cps),
End =
case EndPos of
Expand Down
55 changes: 55 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -7328,3 +7328,58 @@ pub fn wave14_arrow_callback_ignores_thisarg_test() {
val("[1, 2, 3].map((x) => x * 2, { k: 9 }).join(',')")
|> should.equal(dyn("2,4,6"))
}

// ==== String (wave 15) ====

/// §22.1.3.22/.7/.24 RequireObjectCoercible: a String.prototype method invoked
/// with a `null`/`undefined` `this` (via `.call`) throws a TypeError before any
/// argument is inspected. Encodes test262 startsWith/endsWith `this-is-null` and
/// `this-is-undefined`.
pub fn string_method_this_not_coercible_throws_test() {
let cases = [
"String.prototype.startsWith.call(null, '')",
"String.prototype.startsWith.call(undefined, '')",
"String.prototype.endsWith.call(null, '')",
"String.prototype.endsWith.call(undefined, '')",
"String.prototype.substring.call(null)",
"String.prototype.substring.call(undefined)",
]
list.each(cases, fn(expr) {
let m = compile("function f() { return " <> expr <> "; }")
let threw = case catch_apply(m, atom.create("f"), []) {
Error(_) -> True
Ok(_) -> False
}
threw |> should.equal(True)
})
}

/// ToString(this) is abrupt when `this` is a Symbol (§7.1.17 step 2), so a
/// String.prototype method called on a Symbol receiver throws a TypeError.
/// Encodes test262 substring/startsWith/endsWith `this-value-tostring-throws-symbol`
/// and `return-abrupt-from-this-as-symbol`. Asserts the thrown value is a
/// TypeError, not merely that it throws.
pub fn string_method_symbol_this_throws_typeerror_test() {
let cases = [
"String.prototype.substring.call(Symbol())",
"String.prototype.startsWith.call(Symbol(), '')",
"String.prototype.endsWith.call(Symbol(), '')",
]
list.each(cases, fn(expr) {
let m =
compile(
"function f() { try { "
<> expr
<> "; return \"no throw\"; } catch (e) { return e.name; } }",
)
call(m, "f", []) |> should.equal(dyn("TypeError"))
})
}

/// A well-formed string receiver still produces the ordinary result — the
/// RequireObjectCoercible/ToString guard does not perturb the normal path.
pub fn string_method_this_guard_preserves_normal_test() {
val("'hello'.startsWith('he') ? 'y' : 'n'") |> should.equal(dyn("y"))
val("'hello'.endsWith('lo') ? 'y' : 'n'") |> should.equal(dyn("y"))
val("'hello'.substring(1, 3)") |> should.equal(dyn("el"))
}
Loading