diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d28e96..0a9a2cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,7 @@ The declared exceptions are the guarded `performance.now()` behind `durationMs`, If the method takes a callback, invoke it through `context.call` so it runs under the budget. Never hand a script closure to a host implementation. 3. If the method needs no callback and cannot observe prototypes, it may delegate to the host implementation on a Maple-owned value (for example `Array.prototype.with`). + Call it through the prototype, as `Array.prototype.with.call(a, …)`, never as `a.with(…)`: a script can give its array an own property of that name. Wrap it in `guardHostErrors` when the spec requires a `RangeError`, `URIError` or `SyntaxError` for it, or when it can build a string or array longer than the host allows. 4. Return only sandbox values: primitives, Maple-created objects and arrays, or wrappers. Never return a native `Date`, `RegExp`, `Map`, `Set` or function. diff --git a/packages/maple/src/builtins/array/array-prototype.ts b/packages/maple/src/builtins/array/array-prototype.ts index 1d44c14..88ca227 100644 --- a/packages/maple/src/builtins/array/array-prototype.ts +++ b/packages/maple/src/builtins/array/array-prototype.ts @@ -40,7 +40,9 @@ import { ARRAY_METHOD_WITH } from './methods/with.ts'; /** * Callbacks always run through the interpreter, so they stay under the budget and the host never - * holds a script function. Methods that mutate refuse read-only input arrays. + * holds a script function. Methods that mutate refuse read-only input arrays. A method that runs + * the host's implementation calls it through `Array.prototype`, never as `a.slice()`, since a + * script can give its array an own property of that name. */ export const ARRAY_PROTOTYPE: Prototype> = new Prototype( 'Array', diff --git a/packages/maple/src/builtins/array/array.spec.ts b/packages/maple/src/builtins/array/array.spec.ts index 26a8391..a6bc66b 100644 --- a/packages/maple/src/builtins/array/array.spec.ts +++ b/packages/maple/src/builtins/array/array.spec.ts @@ -278,6 +278,63 @@ describe('array methods', () => { ).toEqual(['1-2', '1,2', '1,2', '1,2']); }); + it('call the host implementation even where the array has an own property of that name', () => { + expect(run('const a = [1, 2, 3]; a.at = 1; return [].at.call(a, -1);')).toBe(3); + expect(run('const a = [1]; a.concat = 1; return [].concat.call(a, [2], 3);')).toEqual([ + 1, 2, 3 + ]); + expect( + run('const a = [1, 2, 3]; a.copyWithin = 1; [].copyWithin.call(a, 0, 1); return [...a];') + ).toEqual([2, 3, 3]); + expect(run('const a = ["x"]; a.entries = 1; return [...[].entries.call(a)];')).toEqual([ + [0, 'x'] + ]); + expect(run('const a = [1, 2]; a.fill = 1; [].fill.call(a, 0); return [...a];')).toEqual([ + 0, 0 + ]); + expect(run('const a = [1, 2]; a.includes = 1; return [].includes.call(a, 2);')).toBe(true); + expect(run('const a = [1, 2]; a.indexOf = 1; return [].indexOf.call(a, 2);')).toBe(1); + expect(run('const a = ["x", "y"]; a.keys = 1; return [...[].keys.call(a)];')).toEqual([0, 1]); + expect( + run( + 'const a = [1, 2, 1]; a.lastIndexOf = 1; return [[].lastIndexOf.call(a, 1), [].lastIndexOf.call(a, 1, 1)];' + ) + ).toEqual([2, 0]); + expect(run('const a = [1, 2]; a.pop = 1; return [[].pop.call(a), [...a]];')).toEqual([ + 2, + [1] + ]); + expect(run('const a = [1]; a.push = 1; [].push.call(a, 2); return [...a];')).toEqual([1, 2]); + expect(run('const a = [1, 2]; a.reverse = 1; [].reverse.call(a); return [...a];')).toEqual([ + 2, 1 + ]); + expect(run('const a = [1, 2]; a.shift = 1; return [[].shift.call(a), [...a]];')).toEqual([ + 1, + [2] + ]); + expect(run('const a = [3, 1, 2]; a.slice = 1; return [].slice.call(a, 1);')).toEqual([1, 2]); + expect( + run( + 'const a = [1, 2, 3]; a.splice = 1; return [[].splice.call(a, 1), [].splice.call(a, 0, 1, 9), [...a]];' + ) + ).toEqual([[2, 3], [1], [9]]); + expect(run('const a = [1, 2]; a.map = 1; return [].toLocaleString.call(a);')).toBe('1,2'); + expect(run('const a = [1, 2]; a.toReversed = 1; return [].toReversed.call(a);')).toEqual([ + 2, 1 + ]); + expect(run('const a = [2, 1]; a.toSorted = 1; return [].toSorted.call(a);')).toEqual([1, 2]); + expect( + run( + 'const a = [1, 2, 3]; a.toSpliced = 1; return [[].toSpliced.call(a, 1), [].toSpliced.call(a, 0, 1, 9)];' + ) + ).toEqual([[1], [9, 2, 3]]); + expect(run('const a = [1]; a.unshift = 1; [].unshift.call(a, 0); return [...a];')).toEqual([ + 0, 1 + ]); + expect(run('const a = ["x"]; a.values = 1; return [...[].values.call(a)];')).toEqual(['x']); + expect(run('const a = [1, 2]; a.with = 1; return [].with.call(a, 0, 9);')).toEqual([9, 2]); + }); + it('mutate script arrays', () => { expect(run('const l = [1]; const n = l.push(2, 3); return [n, l];')).toEqual([3, [1, 2, 3]]); expect(run('const l = [1, 2]; return [l.pop(), l];')).toEqual([2, [1]]); diff --git a/packages/maple/src/builtins/array/methods/at.ts b/packages/maple/src/builtins/array/methods/at.ts index c7148ef..de0e567 100644 --- a/packages/maple/src/builtins/array/methods/at.ts +++ b/packages/maple/src/builtins/array/methods/at.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_AT: Method> = method( Array.prototype.at.length, - (a, [index]) => a.at(toNumber(index)) + (a, [index]) => Array.prototype.at.call(a, toNumber(index)) ); diff --git a/packages/maple/src/builtins/array/methods/concat.ts b/packages/maple/src/builtins/array/methods/concat.ts index 6fbd505..3f0852a 100644 --- a/packages/maple/src/builtins/array/methods/concat.ts +++ b/packages/maple/src/builtins/array/methods/concat.ts @@ -2,5 +2,5 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts' export const ARRAY_METHOD_CONCAT: Method> = method( Array.prototype.concat.length, - (a, items, { fail }) => guardHostErrors(fail, () => a.concat(...items)) + (a, items, { fail }) => guardHostErrors(fail, () => Array.prototype.concat.call(a, ...items)) ); diff --git a/packages/maple/src/builtins/array/methods/copy-within.ts b/packages/maple/src/builtins/array/methods/copy-within.ts index 76f827e..ead9184 100644 --- a/packages/maple/src/builtins/array/methods/copy-within.ts +++ b/packages/maple/src/builtins/array/methods/copy-within.ts @@ -9,6 +9,8 @@ export const ARRAY_METHOD_COPY_WITHIN: Method> = method( const to = toNumber(target); const from = toNumber(start); const until = optionalNumberArgument(end); - return guardIntegrityErrors(context.fail, a, () => a.copyWithin(to, from, until)); + return guardIntegrityErrors(context.fail, a, () => + Array.prototype.copyWithin.call(a, to, from, until) + ); } ); diff --git a/packages/maple/src/builtins/array/methods/entries.ts b/packages/maple/src/builtins/array/methods/entries.ts index 532410d..7b0f24b 100644 --- a/packages/maple/src/builtins/array/methods/entries.ts +++ b/packages/maple/src/builtins/array/methods/entries.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_ENTRIES: Method> = method( Array.prototype.entries.length, - (a) => new IteratorWrapper(a.entries()) + (a) => new IteratorWrapper(Array.prototype.entries.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/fill.ts b/packages/maple/src/builtins/array/methods/fill.ts index 6763801..7e8e098 100644 --- a/packages/maple/src/builtins/array/methods/fill.ts +++ b/packages/maple/src/builtins/array/methods/fill.ts @@ -7,6 +7,8 @@ export const ARRAY_METHOD_FILL: Method> = method( (a, [value, start, end], context) => { const from = optionalNumberArgument(start); const to = optionalNumberArgument(end); - return guardIntegrityErrors(context.fail, a, () => a.fill(value, from, to)); + return guardIntegrityErrors(context.fail, a, () => + Array.prototype.fill.call(a, value, from, to) + ); } ); diff --git a/packages/maple/src/builtins/array/methods/includes.ts b/packages/maple/src/builtins/array/methods/includes.ts index 56ed975..62c78f9 100644 --- a/packages/maple/src/builtins/array/methods/includes.ts +++ b/packages/maple/src/builtins/array/methods/includes.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_INCLUDES: Method> = method( Array.prototype.includes.length, - (a, [value, from]) => a.includes(value, optionalNumberArgument(from)) + (a, [value, from]) => Array.prototype.includes.call(a, value, optionalNumberArgument(from)) ); diff --git a/packages/maple/src/builtins/array/methods/index-of.ts b/packages/maple/src/builtins/array/methods/index-of.ts index aae6943..cdd5e23 100644 --- a/packages/maple/src/builtins/array/methods/index-of.ts +++ b/packages/maple/src/builtins/array/methods/index-of.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_INDEX_OF: Method> = method( Array.prototype.indexOf.length, - (a, [value, from]) => a.indexOf(value, optionalNumberArgument(from)) + (a, [value, from]) => Array.prototype.indexOf.call(a, value, optionalNumberArgument(from)) ); diff --git a/packages/maple/src/builtins/array/methods/keys.ts b/packages/maple/src/builtins/array/methods/keys.ts index 48b36b0..ec6105e 100644 --- a/packages/maple/src/builtins/array/methods/keys.ts +++ b/packages/maple/src/builtins/array/methods/keys.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_KEYS: Method> = method( Array.prototype.keys.length, - (a) => new IteratorWrapper(a.keys()) + (a) => new IteratorWrapper(Array.prototype.keys.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/last-index-of.ts b/packages/maple/src/builtins/array/methods/last-index-of.ts index 2140a6a..b804dd2 100644 --- a/packages/maple/src/builtins/array/methods/last-index-of.ts +++ b/packages/maple/src/builtins/array/methods/last-index-of.ts @@ -4,5 +4,7 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_LAST_INDEX_OF: Method> = method( Array.prototype.lastIndexOf.length, (a, args) => - args.length < 2 ? a.lastIndexOf(args[0]) : a.lastIndexOf(args[0], toNumber(args[1])) + args.length < 2 + ? Array.prototype.lastIndexOf.call(a, args[0]) + : Array.prototype.lastIndexOf.call(a, args[0], toNumber(args[1])) ); diff --git a/packages/maple/src/builtins/array/methods/pop.ts b/packages/maple/src/builtins/array/methods/pop.ts index 2b7c087..bd270a1 100644 --- a/packages/maple/src/builtins/array/methods/pop.ts +++ b/packages/maple/src/builtins/array/methods/pop.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_POP: Method> = method( Array.prototype.pop.length, - (a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.pop()) + (a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.pop.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/push.ts b/packages/maple/src/builtins/array/methods/push.ts index c8d3886..0c3029b 100644 --- a/packages/maple/src/builtins/array/methods/push.ts +++ b/packages/maple/src/builtins/array/methods/push.ts @@ -5,6 +5,6 @@ export const ARRAY_METHOD_PUSH: Method> = method( Array.prototype.push.length, (a, items, context) => guardHostErrors(context.fail, () => - guardIntegrityErrors(context.fail, a, () => a.push(...items)) + guardIntegrityErrors(context.fail, a, () => Array.prototype.push.call(a, ...items)) ) ); diff --git a/packages/maple/src/builtins/array/methods/reverse.ts b/packages/maple/src/builtins/array/methods/reverse.ts index 13272a3..a922235 100644 --- a/packages/maple/src/builtins/array/methods/reverse.ts +++ b/packages/maple/src/builtins/array/methods/reverse.ts @@ -5,6 +5,5 @@ export const ARRAY_METHOD_REVERSE: Method> = method( Array.prototype.reverse.length, (a, _args, context) => // The host's own reverse runs no script code and keeps holes where JavaScript puts them - // oxlint-disable-next-line unicorn/no-array-reverse - reversing in place is what this method does - guardIntegrityErrors(context.fail, a, () => a.reverse()) + guardIntegrityErrors(context.fail, a, () => Array.prototype.reverse.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/shift.ts b/packages/maple/src/builtins/array/methods/shift.ts index 62913a8..d8f5063 100644 --- a/packages/maple/src/builtins/array/methods/shift.ts +++ b/packages/maple/src/builtins/array/methods/shift.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_SHIFT: Method> = method( Array.prototype.shift.length, - (a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.shift()) + (a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.shift.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/slice.ts b/packages/maple/src/builtins/array/methods/slice.ts index feb0ff3..affc1b6 100644 --- a/packages/maple/src/builtins/array/methods/slice.ts +++ b/packages/maple/src/builtins/array/methods/slice.ts @@ -3,5 +3,6 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_SLICE: Method> = method( Array.prototype.slice.length, - (a, [start, end]) => a.slice(optionalNumberArgument(start), optionalNumberArgument(end)) + (a, [start, end]) => + Array.prototype.slice.call(a, optionalNumberArgument(start), optionalNumberArgument(end)) ); diff --git a/packages/maple/src/builtins/array/methods/splice.ts b/packages/maple/src/builtins/array/methods/splice.ts index bb01f4a..5a76e1c 100644 --- a/packages/maple/src/builtins/array/methods/splice.ts +++ b/packages/maple/src/builtins/array/methods/splice.ts @@ -2,12 +2,17 @@ import { guardIntegrityErrors } from '../../../values/guard-integrity-errors.ts' import { method, type Method } from '../../../values/native.ts'; import { splice } from '../utils/splice.ts'; +// `call` is typed by the last overload, which needs a delete count, so `splice(start)` gets its own +const spliceToEnd: (this: Array, start: number) => Array = Array.prototype.splice; + export const ARRAY_METHOD_SPLICE: Method> = method( Array.prototype.splice.length, (a, args, context) => { return splice(a, args, (start, count, items) => { return guardIntegrityErrors(context.fail, a, () => - count === undefined ? a.splice(start) : a.splice(start, count, ...items) + count === undefined + ? spliceToEnd.call(a, start) + : Array.prototype.splice.call(a, start, count, ...items) ); }); } diff --git a/packages/maple/src/builtins/array/methods/to-locale-string.ts b/packages/maple/src/builtins/array/methods/to-locale-string.ts index 8092a6d..f41da9e 100644 --- a/packages/maple/src/builtins/array/methods/to-locale-string.ts +++ b/packages/maple/src/builtins/array/methods/to-locale-string.ts @@ -8,7 +8,7 @@ export const ARRAY_METHOD_TO_LOCALE_STRING: Method> = method( (a, [locales, options], { fail }) => { const tags = localesArgument(locales); const intlOptions = intlOptionsArgument(options); - const mapped = a.map((element) => { + const mapped = Array.prototype.map.call(a, (element) => { if (element === null || element === undefined) { return ''; } diff --git a/packages/maple/src/builtins/array/methods/to-reversed.ts b/packages/maple/src/builtins/array/methods/to-reversed.ts index fa83e7b..c0681f1 100644 --- a/packages/maple/src/builtins/array/methods/to-reversed.ts +++ b/packages/maple/src/builtins/array/methods/to-reversed.ts @@ -2,5 +2,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_TO_REVERSED: Method> = method( Array.prototype.toReversed.length, - (a) => a.toReversed() + (a) => Array.prototype.toReversed.call(a) ); diff --git a/packages/maple/src/builtins/array/methods/to-sorted.ts b/packages/maple/src/builtins/array/methods/to-sorted.ts index c0f85a9..f651d21 100644 --- a/packages/maple/src/builtins/array/methods/to-sorted.ts +++ b/packages/maple/src/builtins/array/methods/to-sorted.ts @@ -3,5 +3,5 @@ import { comparator } from '../utils/comparator.ts'; export const ARRAY_METHOD_TO_SORTED: Method> = method( Array.prototype.toSorted.length, - (a, [fn], context) => a.toSorted(comparator(fn, context)) + (a, [fn], context) => Array.prototype.toSorted.call(a, comparator(fn, context)) ); diff --git a/packages/maple/src/builtins/array/methods/to-spliced.ts b/packages/maple/src/builtins/array/methods/to-spliced.ts index 7f63459..e87658e 100644 --- a/packages/maple/src/builtins/array/methods/to-spliced.ts +++ b/packages/maple/src/builtins/array/methods/to-spliced.ts @@ -1,11 +1,21 @@ import { method, type Method } from '../../../values/native.ts'; import { splice } from '../utils/splice.ts'; +// `call` is typed by the last overload, which takes no items, so the one with items gets its own +const toSplicedWithItems: ( + this: ReadonlyArray, + start: number, + skipCount: number, + ...items: Array +) => Array = Array.prototype.toSpliced; + export const ARRAY_METHOD_TO_SPLICED: Method> = method( Array.prototype.toSpliced.length, (a, args) => { return splice(a, args, (start, count, items) => { - return count === undefined ? a.toSpliced(start) : a.toSpliced(start, count, ...items); + return count === undefined + ? Array.prototype.toSpliced.call(a, start) + : toSplicedWithItems.call(a, start, count, ...items); }); } ); diff --git a/packages/maple/src/builtins/array/methods/unshift.ts b/packages/maple/src/builtins/array/methods/unshift.ts index 2bdb6cc..81f5e5f 100644 --- a/packages/maple/src/builtins/array/methods/unshift.ts +++ b/packages/maple/src/builtins/array/methods/unshift.ts @@ -5,6 +5,6 @@ export const ARRAY_METHOD_UNSHIFT: Method> = method( Array.prototype.unshift.length, (a, items, context) => guardHostErrors(context.fail, () => - guardIntegrityErrors(context.fail, a, () => a.unshift(...items)) + guardIntegrityErrors(context.fail, a, () => Array.prototype.unshift.call(a, ...items)) ) ); diff --git a/packages/maple/src/builtins/array/methods/values.ts b/packages/maple/src/builtins/array/methods/values.ts index 670d718..1ee1c10 100644 --- a/packages/maple/src/builtins/array/methods/values.ts +++ b/packages/maple/src/builtins/array/methods/values.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_VALUES: Method> = method( Array.prototype.values.length, - (a) => new IteratorWrapper(a.values()) + (a) => new IteratorWrapper(Array.prototype.values.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/with.ts b/packages/maple/src/builtins/array/methods/with.ts index 3ffc491..0bd5415 100644 --- a/packages/maple/src/builtins/array/methods/with.ts +++ b/packages/maple/src/builtins/array/methods/with.ts @@ -3,5 +3,6 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts' export const ARRAY_METHOD_WITH: Method> = method( Array.prototype.with.length, - (a, [index, value], { fail }) => guardHostErrors(fail, () => a.with(toNumber(index), value)) + (a, [index, value], { fail }) => + guardHostErrors(fail, () => Array.prototype.with.call(a, toNumber(index), value)) );