fix(codemode): coerce match/search patterns, allow any for...in target, bind the last duplicate parameter (#50802)

This commit is contained in:
Aiden Cline
2026-09-22 23:32:27 -05:00
committed by GitHub
parent 150dc69e4b
commit 68b28bdb98
6 changed files with 53 additions and 28 deletions
+8 -6
View File
@@ -41,8 +41,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`.
- [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at
parse time. Without it, `yield` is an ordinary binding.
- [ ] Duplicate parameter names in non-strict code throw when the function is called, instead of binding the last
parameter as JavaScript does.
- [x] Duplicate parameter names in non-strict code bind the last parameter, as in JS (`function f(a, a)` called
with `(1, 2)` sees `a === 2`).
## Values and literals
@@ -89,7 +89,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
sources are rejected.
- [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and
`const { slice } = values` find the inherited built-in.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
- [x] Any assignment target as a `for...in` head, like `for...of`: `for (x.y in obj)`, `for (a[i++] in obj)`, and
destructuring patterns.
## Statements and control flow
@@ -354,11 +355,12 @@ reject }` object.
native JS, `split(undefined)` returns the whole string, and `includes`/`startsWith`/`endsWith` reject regular
expressions with a native-style `TypeError`. Opaque runtime references still reject as data errors, and
`repeat` still requires a finite non-negative count.
- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. Present
arguments must still be a regular expression or string pattern.
- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern.
- [x] `String.raw`, on a template object or any `{ raw }` object; raw strings and substitutions coerce through their own
`toString`.
- [ ] `match`, `search`, and `split` accept any value and coerce it (objects via `toString`), like JavaScript.
- [x] `match`, `matchAll`, `search`, and `split` read any non-RegExp argument as a pattern string, as `new RegExp(arg)`
would: `"a1b".match(1)` matches `/1/`, `search(null)` looks for `"null"`, and `undefined` is the empty pattern.
Objects use their built-in string form until ToPrimitive lands.
## Numbers and Math
@@ -937,10 +937,10 @@ class Frame<R> {
const keys = self.enumerableKeys(right, node.right)
if (left.type !== "Identifier" && left.type !== "VariableDeclaration") {
if (left.type === "RestElement" || left.type === "AssignmentPattern") {
throw typeError("Unsupported for...in binding.", left)
}
const assignmentName = left.type === "Identifier" ? left.name : undefined
const assignment = left.type === "VariableDeclaration" ? undefined : left
for (const key of keys) {
const result = yield* Effect.gen(function* () {
@@ -950,8 +950,8 @@ class Frame<R> {
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, true)
} else if (declared) {
yield* self.assignPattern(declared.pattern, key, left)
} else if (assignmentName) {
self.scopes.set(assignmentName, key, left)
} else if (assignment) {
yield* self.assignPattern(assignment, key, left)
}
return yield* self.evaluateStatement(node.body)
}).pipe(
@@ -1672,6 +1672,9 @@ class Frame<R> {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
}
const parameters = fn.parameters.map((parameter) =>
parameter.type === "Identifier" ? parameter.name : undefined,
)
const bind = Effect.gen(function* () {
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
@@ -1684,6 +1687,8 @@ class Frame<R> {
)
break
}
// A sloppy simple parameter list may repeat a name; the last occurrence wins, as in JS.
if (parameter.type === "Identifier" && parameters.lastIndexOf(parameter.name) !== index) continue
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
}
})
+8 -13
View File
@@ -33,21 +33,16 @@ const escapeRegexHint =
'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.'
export const toHostRegex = (arg: Value, method: string, extraFlags = ""): RegExp => {
// Native parity: an undefined pattern behaves as an empty pattern.
if (arg === undefined) return new RegExp("", extraFlags)
if (arg instanceof RegExpObj) return arg.regex
if (typeof arg === "string") {
try {
return new RegExp(arg, extraFlags)
} catch (error) {
throw syntaxError(
`String.${method} received the string ${JSON.stringify(arg)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`,
)
}
// Anything else is a pattern string, as `new RegExp(arg)` would read it: undefined is the empty pattern.
const source = arg === undefined ? "" : coerceToString(arg)
try {
return new RegExp(source, extraFlags)
} catch (error) {
throw syntaxError(
`String.${method} received the string ${JSON.stringify(source)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`,
)
}
throw typeError(
`String.${method} expects a regular expression (a /pattern/flags literal or new RegExp(...)) or a string pattern, not ${arg === null ? "null" : typeof arg}.`,
)
}
export const matchToValue = (builtins: Builtins, match: RegExpMatchArray): Arr => {
+22
View File
@@ -1156,3 +1156,25 @@ describe("String.raw", () => {
expect(failure.message).toContain("String.raw expects a template object with a raw array")
})
})
describe("sloppy duplicate parameters and for...in targets", () => {
test("a repeated parameter name binds the last argument", async () => {
expect(await value(`function f(a, b, a) { return [a, b] } return [f(1, 2, 3), f(1)]`)).toEqual([
[3, 2],
[null, null],
])
})
test("for...in assigns to any target: members, computed members, and patterns", async () => {
expect(
await value(`
const x = {}, seen = [], a = []
let i = 0, first
for (x.y in { p: 1, q: 2 }) seen.push(x.y)
for (a[i++] in { p: 1, q: 2 });
for ([first] in { ab: 1 });
return [seen, x.y, a, first]
`),
).toEqual([["p", "q"], "q", ["p", "q"], "a"])
})
})
+6 -4
View File
@@ -452,10 +452,12 @@ describe("RegExp", () => {
expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
})
test("a non-pattern argument names the expected shapes", async () => {
const err = await error(`return "abc".match(42)`)
expect(err.message).toContain("expects a regular expression")
expect(err.message).toContain("not number")
test("any argument is a pattern string, as new RegExp(arg) reads it", async () => {
expect(
await value(
`return ["a42b".match(42)[0], "xnullx".search(null), "abc".match(undefined), [..."1a1".matchAll(1)].length]`,
),
).toEqual(["42", 1, [""], 2])
})
test("source and flags properties read through", async () => {
@@ -215,7 +215,6 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeErr
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-in/head-lhs-member.js # Unsupported for...in binding.
language/statements/for-of/dstr/array-elem-iter-rtrn-close-err.js # Iterator next must be a function.
language/statements/for-of/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/statements/for-of/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true