Skip to content

Commit

Permalink
Normative: Throw on duplicates in PrepareTemporalFields
Browse files Browse the repository at this point in the history
It should be an error to return duplicate fields in a Calendar's field()
method, thus we throw if we encounter one in PrepareTemporalFields.

There are two cases (PlainMonthDay.prototype.toPlainDate and
PlainYearMonth.prototype.toPlainDate) where, after checking on the
return values of field() in previous invocations of
PrepareTemporalFields, we want to deduplicate the field name list in a
later call to PrepareTemporalFields after concatenating them, since we
sort them in PrepareTemporalFields. For this reason, we add a
duplicateBehavior parameter to PrepareTemporalFields. This allows us to
remove the MergeLists abstract operation, now replaced by a simple list
concatenation and deduplication in PrepareTemporalFields with
duplicateBehavior set to ignore.

It should also be an error to return field names 'constructor' or
'__proto__' in a Calendar's field() method, and we throw if that
happens.

Fixes #2532, 2576.
  • Loading branch information
guijemont committed May 16, 2023
1 parent 31656b1 commit 93a07b2
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 96 deletions.
34 changes: 22 additions & 12 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,27 +1060,37 @@ export function PrepareTemporalFields(
bag,
fields,
requiredFields,
duplicateBehaviour = 'throw',
{ emptySourceErrorMessage = 'no supported properties found' } = {}
) {
const result = ObjectCreate(null);
let any = false;
Call(ArrayPrototypeSort, fields, []);
let previousProperty = undefined;
for (let index = 0; index < fields.length; index++) {
const property = fields[index];
let value = bag[property];
if (value !== undefined) {
any = true;
if (BUILTIN_CASTS.has(property)) {
value = BUILTIN_CASTS.get(property)(value);
}
result[property] = value;
} else if (requiredFields !== 'partial') {
if (Call(ArrayIncludes, requiredFields, [property])) {
throw new TypeError(`required property '${property}' missing or undefined`);
if (property === 'constructor' || property === '__proto__') {
throw new RangeError(`Calendar fields cannot be named ${property}`);
}
if (property !== previousProperty) {
let value = bag[property];
if (value !== undefined) {
any = true;
if (BUILTIN_CASTS.has(property)) {
value = BUILTIN_CASTS.get(property)(value);
}
result[property] = value;
} else if (requiredFields !== 'partial') {
if (Call(ArrayIncludes, requiredFields, [property])) {
throw new TypeError(`required property '${property}' missing or undefined`);
}
value = BUILTIN_DEFAULTS.get(property);
result[property] = value;
}
value = BUILTIN_DEFAULTS.get(property);
result[property] = value;
} else if (duplicateBehaviour === 'throw') {
throw new RangeError('Duplicate calendar fields');
}
previousProperty = property;
}
if (requiredFields === 'partial' && !any) {
throw new TypeError(emptySourceErrorMessage);
Expand Down
20 changes: 3 additions & 17 deletions polyfill/lib/plainmonthday.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { DateTimeFormat } from './intl.mjs';
import { MakeIntrinsicClass } from './intrinsicclass.mjs';
import { ISO_MONTH, ISO_DAY, ISO_YEAR, CALENDAR, GetSlot } from './slots.mjs';

const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeConcat = Array.prototype.concat;
const ObjectCreate = Object.create;
const SetPrototypeAdd = Set.prototype.add;
const SetPrototypeForEach = Set.prototype.forEach;

export class PlainMonthDay {
constructor(isoMonth, isoDay, calendar = 'iso8601', referenceISOYear = 1972) {
Expand Down Expand Up @@ -84,20 +82,8 @@ export class PlainMonthDay {
const inputFieldNames = ES.CalendarFields(calendar, ['year']);
const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []);
let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields);

// TODO: Use MergeLists abstract operation.
const uniqueFieldNames = new Set();
for (let index = 0; index < receiverFieldNames.length; index++) {
ES.Call(SetPrototypeAdd, uniqueFieldNames, [receiverFieldNames[index]]);
}
for (let index = 0; index < inputFieldNames.length; index++) {
ES.Call(SetPrototypeAdd, uniqueFieldNames, [inputFieldNames[index]]);
}
const mergedFieldNames = [];
ES.Call(SetPrototypeForEach, uniqueFieldNames, [
(element) => ES.Call(ArrayPrototypePush, mergedFieldNames, [element])
]);
mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, []);
const concatenatedFieldNames = ES.Call(ArrayPrototypeConcat, receiverFieldNames, inputFieldNames);
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore');
const options = ObjectCreate(null);
options.overflow = 'reject';
return ES.CalendarDateFromFields(calendar, mergedFields, options);
Expand Down
20 changes: 3 additions & 17 deletions polyfill/lib/plainyearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { DateTimeFormat } from './intl.mjs';
import { MakeIntrinsicClass } from './intrinsicclass.mjs';
import { ISO_YEAR, ISO_MONTH, ISO_DAY, CALENDAR, GetSlot } from './slots.mjs';

const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeConcat = Array.prototype.concat;
const ObjectCreate = Object.create;
const SetPrototypeAdd = Set.prototype.add;
const SetPrototypeForEach = Set.prototype.forEach;

export class PlainYearMonth {
constructor(isoYear, isoMonth, calendar = 'iso8601', referenceISODay = 1) {
Expand Down Expand Up @@ -126,20 +124,8 @@ export class PlainYearMonth {
const inputFieldNames = ES.CalendarFields(calendar, ['day']);
const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []);
let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields);

// TODO: Use MergeLists abstract operation.
const uniqueFieldNames = new Set();
for (let index = 0; index < receiverFieldNames.length; index++) {
ES.Call(SetPrototypeAdd, uniqueFieldNames, [receiverFieldNames[index]]);
}
for (let index = 0; index < inputFieldNames.length; index++) {
ES.Call(SetPrototypeAdd, uniqueFieldNames, [inputFieldNames[index]]);
}
const mergedFieldNames = [];
ES.Call(SetPrototypeForEach, uniqueFieldNames, [
(element) => ES.Call(ArrayPrototypePush, mergedFieldNames, [element])
]);
mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, []);
const concatenatedFieldNames = ES.Call(ArrayPrototypeConcat, receiverFieldNames, inputFieldNames);
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore');
const options = ObjectCreate(null);
options.overflow = 'reject';
return ES.CalendarDateFromFields(calendar, mergedFields, options);
Expand Down
51 changes: 30 additions & 21 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,7 @@ <h1>
_fields_: an Object,
_fieldNames_: a List of property names,
_requiredFields_: ~partial~ or a List of property names,
optional _duplicateBehaviour_: ~throw~ or ~ignore~,
): either a normal completion containing an Object, or an abrupt completion
</h1>
<dl class="header">
Expand All @@ -1810,31 +1811,39 @@ <h1>
</dd>
</dl>
<emu-alg>
1. If _duplicateBehaviour_ is not present, set _duplicateBehaviour_ to ~throw~.
1. Let _result_ be OrdinaryObjectCreate(*null*).
1. Let _any_ be *false*.
1. Let _sortedFieldNames_ be SortStringListByCodeUnit(_fieldNames_).
1. Let _previousProperty_ be *undefined*.
1. For each property name _property_ of _sortedFieldNames_, do
1. Let _value_ be ? Get(_fields_, _property_).
1. If _value_ is not *undefined*, then
1. Set _any_ to *true*.
1. If _property_ is in the Property column of <emu-xref href="#table-temporal-field-requirements"></emu-xref> and there is a Conversion value in the same row, then
1. Let _Conversion_ be the Conversion value of the same row.
1. If _Conversion_ is ~ToIntegerWithTruncation~, then
1. Set _value_ to ? ToIntegerWithTruncation(_value_).
1. Set _value_ to 𝔽(_value_).
1. Else if _Conversion_ is ~ToPositiveIntegerWithTruncation~, then
1. Set _value_ to ? ToPositiveIntegerWithTruncation(_value_).
1. Set _value_ to 𝔽(_value_).
1. Else,
1. Assert: _Conversion_ is ~ToString~.
1. Set _value_ to ? ToString(_value_).
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
1. Else if _requiredFields_ is a List, then
1. If _requiredFields_ contains _property_, then
1. Throw a *TypeError* exception.
1. If _property_ is in the Property column of <emu-xref href="#table-temporal-field-requirements"></emu-xref>, then
1. Set _value_ to the corresponding Default value of the same row.
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
1. If _property_ is one of *"constructor"* or *"__proto__"*, then
1. Throw a *RangeError* exception.
1. If _property_ is not equal to _previousProperty_, then
1. Let _value_ be ? Get(_fields_, _property_).
1. If _value_ is not *undefined*, then
1. Set _any_ to *true*.
1. If _property_ is in the Property column of <emu-xref href="#table-temporal-field-requirements"></emu-xref> and there is a Conversion value in the same row, then
1. Let _Conversion_ be the Conversion value of the same row.
1. If _Conversion_ is ~ToIntegerWithTruncation~, then
1. Set _value_ to ? ToIntegerWithTruncation(_value_).
1. Set _value_ to 𝔽(_value_).
1. Else if _Conversion_ is ~ToPositiveIntegerWithTruncation~, then
1. Set _value_ to ? ToPositiveIntegerWithTruncation(_value_).
1. Set _value_ to 𝔽(_value_).
1. Else,
1. Assert: _Conversion_ is ~ToString~.
1. Set _value_ to ? ToString(_value_).
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
1. Else if _requiredFields_ is a List, then
1. If _requiredFields_ contains _property_, then
1. Throw a *TypeError* exception.
1. If _property_ is in the Property column of <emu-xref href="#table-temporal-field-requirements"></emu-xref>, then
1. Set _value_ to the corresponding Default value of the same row.
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
1. Else if _duplicateBehaviour_ is ~throw~, then
1. Throw a *RangeError* exception.
1. Set _previousProperty_ to _property_.
1. If _requiredFields_ is ~partial~ and _any_ is *false*, then
1. Throw a *TypeError* exception.
1. Return _result_.
Expand Down
25 changes: 0 additions & 25 deletions spec/mainadditions.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,6 @@ <h1>The Year-Week Record Specification Type</h1>
</emu-clause>
</ins>

<ins class="block">
<emu-clause id="sec-temporal-mergelists" type="abstract operation">
<h1>
MergeLists (
_a_: a List,
_b_: a List,
): a List
</h1>
<dl class="header">
<dt>description</dt>
<dd>It returns the list-concatenation of its arguments, with duplicate elements removed.</dd>
</dl>
<emu-alg>
1. Let _merged_ be a new empty List.
1. For each element _element_ of _a_, do
1. If _merged_ does not contain _element_, then
1. Append _element_ to _merged_.
1. For each element _element_ of _b_, do
1. If _merged_ does not contain _element_, then
1. Append _element_ to _merged_.
1. Return _merged_.
</emu-alg>
</emu-clause>
</ins>

<ins class="block">
<emu-clause id="sec-sortstringlistbycodeunit" type="abstract operation">
<h1>
Expand Down
4 changes: 2 additions & 2 deletions spec/plainmonthday.html
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ <h1>Temporal.PlainMonthDay.prototype.toPlainDate ( _item_ )</h1>
1. Let _inputFieldNames_ be ? CalendarFields(_calendar_, « *"year"* »).
1. Let _inputFields_ be ? PrepareTemporalFields(_item_, _inputFieldNames_, «»).
1. Let _mergedFields_ be ? CalendarMergeFields(_calendar_, _fields_, _inputFields_).
1. Let _mergedFieldNames_ be MergeLists(_receiverFieldNames_, _inputFieldNames_).
1. Set _mergedFields_ to ? PrepareTemporalFields(_mergedFields_, _mergedFieldNames_, «»).
1. Let _concatenatedFieldNames_ be the list-concatenation of _receiverFieldNames_ and _inputFieldNames_.
1. Set _mergedFields_ to ? PrepareTemporalFields(_mergedFields_, _concatenatedFieldNames_, «», ~ignore~).
1. Let _options_ be OrdinaryObjectCreate(*null*).
1. Perform ! CreateDataPropertyOrThrow(_options_, *"overflow"*, *"reject"*).
1. Return ? CalendarDateFromFields(_calendar_, _mergedFields_, _options_).
Expand Down
4 changes: 2 additions & 2 deletions spec/plainyearmonth.html
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,8 @@ <h1>Temporal.PlainYearMonth.prototype.toPlainDate ( _item_ )</h1>
1. Let _inputFieldNames_ be ? CalendarFields(_calendar_, « *"day"* »).
1. Let _inputFields_ be ? PrepareTemporalFields(_item_, _inputFieldNames_, «»).
1. Let _mergedFields_ be ? CalendarMergeFields(_calendar_, _fields_, _inputFields_).
1. Let _mergedFieldNames_ be MergeLists(_receiverFieldNames_, _inputFieldNames_).
1. Set _mergedFields_ to ? PrepareTemporalFields(_mergedFields_, _mergedFieldNames_, «»).
1. Let _concatenatedFieldNames_ be the list-concatenation of _receiverFieldNames_ and _inputFieldNames_.
1. Set _mergedFields_ to ? PrepareTemporalFields(_mergedFields_, _concatenatedFieldNames_, «», ~ignore~).
1. Let _options_ be OrdinaryObjectCreate(*null*).
1. Perform ! CreateDataPropertyOrThrow(_options_, *"overflow"*, *"reject"*).
1. Return ? CalendarDateFromFields(_calendar_, _mergedFields_, _options_).
Expand Down

0 comments on commit 93a07b2

Please sign in to comment.