feat(codemode): add substr, well-formed, and Date string methods (#48275)

This commit is contained in:
Aiden Cline
2026-09-09 23:29:34 -05:00
committed by GitHub
parent 1417976257
commit 1452aadc87
6 changed files with 207 additions and 8 deletions
+4 -2
View File
@@ -250,12 +250,13 @@ ultimate source of truth.
## Strings
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`.
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
- [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`.
- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
- [x] `localeCompare`; locale and options arguments are currently ignored.
- [x] `isWellFormed` and `toWellFormed`.
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like
@@ -316,6 +317,7 @@ ultimate source of truth.
- [x] Local and UTC Date setters, including native argument coercion, mutation, rollover, invalid-Date recovery, and
`TimeClip` behavior.
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] `toDateString` and `toTimeString` in the host's local timezone.
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
representation for the string primitive.
@@ -118,9 +118,11 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
result = value.trim()
break
case "trimStart":
case "trimLeft":
result = value.trimStart()
break
case "trimEnd":
case "trimRight":
result = value.trimEnd()
break
// Locale/options are deliberately unsupported; comparison uses the host default locale.
@@ -241,6 +243,15 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
case "substring":
result = value.substring(optNum(0) ?? 0, optNum(1))
break
case "substr":
result = value.substr(optNum(0) ?? 0, optNum(1))
break
case "isWellFormed":
result = value.isWellFormed()
break
case "toWellFormed":
result = value.toWellFormed()
break
case "charCodeAt":
result = value.charCodeAt(optNum(0) ?? 0)
break
+6
View File
@@ -29,6 +29,8 @@ export const dateMethods = new Set([
"toISOString",
"toJSON",
"toString",
"toDateString",
"toTimeString",
"toUTCString",
"toGMTString",
"getFullYear",
@@ -103,6 +105,10 @@ export const invokeDateMethod = (
return Number.isFinite(value.time) ? hosted.toISOString() : null
case "toString":
return coerceToString(value)
case "toDateString":
return hosted.toDateString()
case "toTimeString":
return hosted.toTimeString()
case "toUTCString":
case "toGMTString":
return hosted.toUTCString()
+5
View File
@@ -8,9 +8,12 @@ export const stringMethods = new Set([
"trim",
"trimStart",
"trimEnd",
"trimLeft",
"trimRight",
"split",
"slice",
"substring",
"substr",
"includes",
"startsWith",
"endsWith",
@@ -32,6 +35,8 @@ export const stringMethods = new Set([
"search",
"localeCompare",
"normalize",
"isWellFormed",
"toWellFormed",
])
const codeUnits = (name: string, op: (...codes: Array<number>) => string) =>
+2 -6
View File
@@ -491,12 +491,8 @@ describe("CodeMode-specific string behavior", () => {
expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
})
test("does not expose obsolete string aliases", async () => {
expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([
"undefined",
"undefined",
"undefined",
])
test("exposes the Annex B string aliases every engine ships", async () => {
expect(await value(`return [" x ".trimLeft(), " x ".trimRight(), "abc".substr(1, 1)]`)).toEqual(["x ", " x", "b"])
})
})
@@ -0,0 +1,179 @@
/**
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/annexB/built-ins/String/prototype/substr/length-falsey.js
* - test/annexB/built-ins/String/prototype/substr/length-negative.js
* - test/annexB/built-ins/String/prototype/substr/length-positive.js
* - test/annexB/built-ins/String/prototype/substr/length-undef.js
* - test/annexB/built-ins/String/prototype/substr/start-negative.js
* - test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js
* - test/built-ins/String/prototype/isWellFormed/returns-boolean.js
* - test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js
* - test/built-ins/Date/prototype/toDateString/format.js
* - test/built-ins/Date/prototype/toDateString/invalid-date.js
* - test/built-ins/Date/prototype/toDateString/negative-year.js
* - test/built-ins/Date/prototype/toTimeString/format.js
* - test/built-ins/Date/prototype/toTimeString/invalid-date.js
*
* Copyright (C) 2016, 2017 the V8 project authors. All rights reserved.
* Copyright (C) 2018 Richard Gibson. All rights reserved.
* Copyright (C) 2022 Jordan Harband. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*
* The `typeof String.prototype.method` checks are replaced with `typeof "".method` because
* CodeMode has no prototype objects.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
describe("String.prototype.substr Test262 parity", () => {
test("test/annexB/built-ins/String/prototype/substr/length-falsey.js", async () => {
expect(
await value(`
return [false, NaN, "", null].flatMap((length) => [0, 1, 2, 3].map((start) => "abc".substr(start, length)))
`),
).toEqual(Array(16).fill(""))
})
test("test/annexB/built-ins/String/prototype/substr/length-negative.js", async () => {
expect(
await value(`
return [0, 1, 2, 3].flatMap((start) => [-1, -2, -3, -4].map((length) => "abc".substr(start, length)))
`),
).toEqual(Array(16).fill(""))
})
test("test/annexB/built-ins/String/prototype/substr/length-positive.js", async () => {
expect(
await value(`
return [0, 1, 2, 3].map((start) => [1, 2, 3, 4].map((length) => "abc".substr(start, length)))
`),
).toEqual([
["a", "ab", "abc", "abc"],
["b", "bc", "bc", "bc"],
["c", "c", "c", "c"],
["", "", "", ""],
])
})
test("test/annexB/built-ins/String/prototype/substr/length-undef.js", async () => {
expect(
await value(`
return [
"abc".substr(0), "abc".substr(1), "abc".substr(2), "abc".substr(3),
"abc".substr(0, undefined), "abc".substr(1, undefined), "abc".substr(2, undefined), "abc".substr(3, undefined),
]
`),
).toEqual(["abc", "bc", "c", "", "abc", "bc", "c", ""])
})
test("test/annexB/built-ins/String/prototype/substr/start-negative.js", async () => {
expect(
await value(`
return ["abc".substr(-1), "abc".substr(-2), "abc".substr(-3), "abc".substr(-4), "abc".substr(-1.1)]
`),
).toEqual(["c", "bc", "abc", "abc", "c"])
})
test("test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js", async () => {
expect(
await value(`
const pair = "\\ud834\\udf06"
return [pair.substr(0), pair.substr(1), pair.substr(2), pair.substr(0, 0), pair.substr(0, 1), pair.substr(0, 2)]
`),
).toEqual(["\ud834\udf06", "\udf06", "", "", "\ud834", "\ud834\udf06"])
})
})
describe("String well-formedness Test262 parity", () => {
test("test/built-ins/String/prototype/isWellFormed/returns-boolean.js", async () => {
expect(
await value(`
const leadingPoo = "\\uD83D"
const trailingPoo = "\\uDCA9"
const wholePoo = leadingPoo + trailingPoo
return [
typeof "".isWellFormed,
("a" + leadingPoo + "c" + leadingPoo + "e").isWellFormed(),
("a" + trailingPoo + "c" + trailingPoo + "e").isWellFormed(),
("a" + trailingPoo + leadingPoo + "d").isWellFormed(),
"a💩c".isWellFormed(),
"a\\uD83D\\uDCA9c".isWellFormed(),
("a" + leadingPoo + trailingPoo + "d").isWellFormed(),
wholePoo.slice(0, 1).isWellFormed(),
wholePoo.slice(1).isWellFormed(),
"abc".isWellFormed(),
"a\\u25A8c".isWellFormed(),
]
`),
).toEqual(["function", false, false, false, true, true, true, false, false, true, true])
})
test("test/built-ins/String/prototype/toWellFormed/returns-well-formed-string.js", async () => {
expect(
await value(`
const replacementChar = "\\uFFFD"
const leadingPoo = "\\uD83D"
const trailingPoo = "\\uDCA9"
const wholePoo = leadingPoo + trailingPoo
return [
typeof "".toWellFormed,
("a" + leadingPoo + "c" + leadingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
("a" + trailingPoo + "c" + trailingPoo + "e").toWellFormed() === "a" + replacementChar + "c" + replacementChar + "e",
("a" + trailingPoo + leadingPoo + "d").toWellFormed() === "a" + replacementChar + replacementChar + "d",
"a💩c".toWellFormed() === "a💩c",
"a\\uD83D\\uDCA9c".toWellFormed() === "a\\uD83D\\uDCA9c",
("a" + leadingPoo + trailingPoo + "d").toWellFormed() === "a" + wholePoo + "d",
wholePoo.slice(0, 1).toWellFormed() === replacementChar,
wholePoo.slice(1).toWellFormed() === replacementChar,
"abc".toWellFormed() === "abc",
"a\\u25A8c".toWellFormed() === "a\\u25A8c",
]
`),
).toEqual(["function", true, true, true, true, true, true, true, true, true, true])
})
})
describe("Date string formatting Test262 parity", () => {
test("test/built-ins/Date/prototype/toDateString/format.js", async () => {
expect(
await value(`
const dateRegExp = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{2} [0-9]{4}$/
return [dateRegExp.test(new Date(0).toDateString()), dateRegExp.test(new Date("0020-01-01T00:00:00Z").toDateString())]
`),
).toEqual([true, true])
})
test("test/built-ins/Date/prototype/toDateString/invalid-date.js", async () => {
expect(await value(`return new Date(NaN).toDateString()`)).toBe("Invalid Date")
})
test("test/built-ins/Date/prototype/toDateString/negative-year.js", async () => {
expect(
await value(`
return ["-000001", "-000012", "-000123", "-001234", "-012345", "-123456"].map(
(year) => new Date(year + "-07-01T00:00Z").toDateString().split(" ")[3],
)
`),
).toEqual(["-0001", "-0012", "-0123", "-1234", "-12345", "-123456"])
})
test("test/built-ins/Date/prototype/toTimeString/format.js", async () => {
expect(
await value(`
const timeRegExp = /^[0-9]{2}:[0-9]{2}:[0-9]{2} GMT[+-][0-9]{4}( \\(.+\\))?$/
return timeRegExp.test(new Date(0).toTimeString())
`),
).toBe(true)
})
test("test/built-ins/Date/prototype/toTimeString/invalid-date.js", async () => {
expect(await value(`return new Date(NaN).toTimeString()`)).toBe("Invalid Date")
})
})